Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@expandai/ai

Package Overview
Dependencies
Maintainers
3
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@expandai/ai - npm Package Compare versions

Comparing version
0.1.0
to
0.1.1
+23
-50
dist/index.cjs

@@ -12,13 +12,14 @@ 'use strict';

// src/tool.ts
var snippetsFormatSchema = zod.z.object({
query: zod.z.string().describe("Query to find relevant content snippets from the page"),
maxSnippets: zod.z.number().int().min(1).max(50).optional().describe("Maximum number of snippets to return (1-50, default: 5)"),
targetSnippetSize: zod.z.number().int().min(100).max(2e3).optional().describe("Target snippet size in characters (100-2000, default: 384)")
var searchParamsSchema = zod.z.object({
query: zod.z.string().describe("Query to find relevant content snippets"),
minScore: zod.z.number().min(0).max(1).default(0.6).describe("Minimum relevance score (0-1)"),
maxResults: zod.z.number().int().min(1).max(50).default(5).describe("Maximum snippets to return")
});
var formatSchema = zod.z.union([
zod.z.literal("markdown").describe("Return as cleaned markdown content"),
zod.z.literal("html").describe("Return as raw HTML content"),
zod.z.literal("summary").describe("Return AI-generated summary of the page content"),
snippetsFormatSchema
]).describe("Output format for the fetched content");
var inputSchema = zod.z.object({
url: zod.z.string().url().describe("The URL to fetch content from"),
includeMeta: zod.z.boolean().default(false).describe("Include page meta tags (title, description, Open Graph)"),
search: searchParamsSchema.optional().describe(
"Semantic search. When provided, returns snippets instead of markdown."
)
});
function createExpandFetchTool(options) {

@@ -31,3 +32,3 @@ const apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY;

}
const { description, defaults, ...clientOptions } = options ?? {};
const { description, ...clientOptions } = options ?? {};
const client = new Expand__default.default({

@@ -37,50 +38,22 @@ ...clientOptions,

});
const defaultFormat = defaults?.format ?? "markdown";
const defaultIncludeMeta = defaults?.includeMeta ?? true;
const defaultScrollFullPage = defaults?.scrollFullPage ?? false;
const inputSchema = zod.z.object({
url: zod.z.string().url().describe("The URL to fetch content from"),
format: formatSchema.default(defaultFormat),
includeMeta: zod.z.boolean().default(defaultIncludeMeta).describe("Whether to include page meta tags in the response"),
scrollFullPage: zod.z.boolean().default(defaultScrollFullPage).describe(
"Whether to scroll the full page to capture lazy-loaded content. Set to true for pages with infinite scroll or dynamic content loading."
)
});
return ai.tool({
description: description ?? "Fetch and extract content from any URL.",
description: description ?? "Fetch and extract content from any URL.\n\nReturns markdown by default. When `search` is provided, returns semantically relevant snippets instead.",
inputSchema,
execute: async (input) => {
const { url, format, includeMeta, scrollFullPage } = input;
const { url, includeMeta, search } = input;
try {
const isSnippets = typeof format === "object" && "query" in format;
const hasSearch = search !== void 0;
const response = await client.fetch({
url,
select: {
markdown: format === "markdown",
html: format === "html",
summary: format === "summary",
snippets: isSnippets ? format : void 0,
markdown: !hasSearch,
snippets: hasSearch ? {
query: search.query,
maxSnippets: search.maxResults,
minScore: search.minScore
} : void 0,
meta: includeMeta
},
...scrollFullPage && { browserConfig: { scrollFullPage: true } }
}
});
const data = response.data;
let content;
if (format === "markdown" && data.markdown) {
content = data.markdown;
} else if (format === "html" && data.html) {
content = data.html;
} else if (format === "summary" && data.summary) {
content = data.summary;
} else if (isSnippets && data.snippets) {
const snippets = data.snippets;
content = snippets.map((s) => `${s.text} (Score: ${s.score})`).join("\n");
} else {
content = "";
}
return {
content,
url: response.data.response.url,
...includeMeta && data.meta ? { meta: data.meta } : {}
};
return response.data;
} catch (error) {

@@ -87,0 +60,0 @@ if (error instanceof Expand__default.default.APIError) {

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/tool.ts"],"names":["z","Expand","tool"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ClientOptions } from '@expandai/sdk'\nimport Expand from '@expandai/sdk'\nimport { tool } from 'ai'\nimport { z } from 'zod'\n\n// Re-export ClientOptions for convenience\nexport type { ClientOptions }\n\n/**\n * Snippets format configuration for semantic search within page content.\n */\nexport interface SnippetsFormat {\n\t/** Query to find relevant content snippets from the page */\n\tquery: string\n\t/** Maximum number of snippets to return (1-50, default: 5) */\n\tmaxSnippets?: number\n\t/** Target snippet size in characters (100-2000, default: 384) */\n\ttargetSnippetSize?: number\n}\n\n/**\n * Output format for the fetched content.\n * - 'markdown': Return as cleaned markdown content\n * - 'html': Return as raw HTML content\n * - 'summary': Return AI-generated summary of the page content\n * - SnippetsFormat: Return relevant content snippets matching a query\n */\nexport type Format = 'markdown' | 'html' | 'summary' | SnippetsFormat\n\n/**\n * Default configuration for the fetch tool.\n * These values are used when the LLM doesn't specify them.\n */\nexport interface FetchToolDefaults {\n\t/** Default output format (default: 'markdown') */\n\tformat?: Format\n\t/** Whether to include page meta tags by default (default: true) */\n\tincludeMeta?: boolean\n\t/** Whether to scroll the full page to capture lazy-loaded content (default: false) */\n\tscrollFullPage?: boolean\n}\n\n/**\n * Configuration options for the Expand fetch tool.\n */\nexport interface ExpandFetchToolOptions extends ClientOptions {\n\t/** Custom description for the tool (overrides default) */\n\tdescription?: string\n\t/** Default values for tool parameters */\n\tdefaults?: FetchToolDefaults\n}\n\n/**\n * Result returned by the fetch tool.\n */\nexport interface FetchResult {\n\t/** The fetched content in the requested format */\n\tcontent: string\n\t/** The final URL after any redirects */\n\turl: string\n\t/** Page metadata (title, description, etc.) if includeMeta was true */\n\tmeta?: {\n\t\ttitle?: string | null\n\t\tdescription?: string | null\n\t\tcanonicalUrl?: string | null\n\t\tlanguage?: string | null\n\t\tcharset?: string | null\n\t}\n}\n\n// Zod schema for snippets format - matches MCP server\nconst snippetsFormatSchema = z.object({\n\tquery: z\n\t\t.string()\n\t\t.describe('Query to find relevant content snippets from the page'),\n\tmaxSnippets: z\n\t\t.number()\n\t\t.int()\n\t\t.min(1)\n\t\t.max(50)\n\t\t.optional()\n\t\t.describe('Maximum number of snippets to return (1-50, default: 5)'),\n\ttargetSnippetSize: z\n\t\t.number()\n\t\t.int()\n\t\t.min(100)\n\t\t.max(2000)\n\t\t.optional()\n\t\t.describe('Target snippet size in characters (100-2000, default: 384)'),\n})\n\n// Zod schema for format parameter - mirrors MCP server exactly\nconst formatSchema = z\n\t.union([\n\t\tz.literal('markdown').describe('Return as cleaned markdown content'),\n\t\tz.literal('html').describe('Return as raw HTML content'),\n\t\tz\n\t\t\t.literal('summary')\n\t\t\t.describe('Return AI-generated summary of the page content'),\n\t\tsnippetsFormatSchema,\n\t])\n\t.describe('Output format for the fetched content')\n\n/**\n * Create an Expand fetch tool for the Vercel AI SDK.\n *\n * The tool mirrors the MCP server's fetch tool, providing consistent behavior\n * across all Expand integrations.\n *\n * @param options - Configuration options (SDK client options + tool-specific options)\n * @returns A Vercel AI SDK tool that fetches and extracts web content\n *\n * @example\n * ```typescript\n * import { createExpandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n * import { openai } from '@ai-sdk/openai'\n *\n * // With default markdown format\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * })\n *\n * // With custom defaults\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * defaults: {\n * format: 'summary',\n * includeMeta: false,\n * },\n * })\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandTool },\n * prompt: 'What is on the Expand homepage?',\n * })\n * ```\n */\nexport function createExpandFetchTool(options?: ExpandFetchToolOptions) {\n\tconst apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY\n\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t'Expand API key is required. Provide it via options.apiKey or set EXPAND_API_KEY environment variable.',\n\t\t)\n\t}\n\n\t// Extract tool-specific options, pass rest to SDK client\n\tconst { description, defaults, ...clientOptions } = options ?? {}\n\n\t// Create Expand client with all client options\n\tconst client = new Expand({\n\t\t...clientOptions,\n\t\tapiKey,\n\t})\n\n\t// Apply defaults\n\tconst defaultFormat: Format = defaults?.format ?? 'markdown'\n\tconst defaultIncludeMeta = defaults?.includeMeta ?? true\n\tconst defaultScrollFullPage = defaults?.scrollFullPage ?? false\n\n\tconst inputSchema = z.object({\n\t\turl: z.string().url().describe('The URL to fetch content from'),\n\t\tformat: formatSchema.default(defaultFormat),\n\t\tincludeMeta: z\n\t\t\t.boolean()\n\t\t\t.default(defaultIncludeMeta)\n\t\t\t.describe('Whether to include page meta tags in the response'),\n\t\tscrollFullPage: z\n\t\t\t.boolean()\n\t\t\t.default(defaultScrollFullPage)\n\t\t\t.describe(\n\t\t\t\t'Whether to scroll the full page to capture lazy-loaded content. Set to true for pages with infinite scroll or dynamic content loading.',\n\t\t\t),\n\t})\n\n\ttype ToolInput = z.infer<typeof inputSchema>\n\n\treturn tool({\n\t\tdescription: description ?? 'Fetch and extract content from any URL.',\n\t\tinputSchema,\n\t\texecute: async (input: ToolInput): Promise<FetchResult> => {\n\t\t\tconst { url, format, includeMeta, scrollFullPage } = input\n\t\t\ttry {\n\t\t\t\t// Determine if format is snippets\n\t\t\t\tconst isSnippets = typeof format === 'object' && 'query' in format\n\n\t\t\t\tconst response = await client.fetch({\n\t\t\t\t\turl,\n\t\t\t\t\tselect: {\n\t\t\t\t\t\tmarkdown: format === 'markdown',\n\t\t\t\t\t\thtml: format === 'html',\n\t\t\t\t\t\tsummary: format === 'summary',\n\t\t\t\t\t\tsnippets: isSnippets ? format : undefined,\n\t\t\t\t\t\tmeta: includeMeta,\n\t\t\t\t\t},\n\t\t\t\t\t...(scrollFullPage && { browserConfig: { scrollFullPage: true } }),\n\t\t\t\t})\n\n\t\t\t\t// Build content based on format\n\t\t\t\tconst data = response.data as unknown as Record<string, unknown>\n\t\t\t\tlet content: string\n\n\t\t\t\tif (format === 'markdown' && data.markdown) {\n\t\t\t\t\tcontent = data.markdown as string\n\t\t\t\t} else if (format === 'html' && data.html) {\n\t\t\t\t\tcontent = data.html as string\n\t\t\t\t} else if (format === 'summary' && data.summary) {\n\t\t\t\t\tcontent = data.summary as string\n\t\t\t\t} else if (isSnippets && data.snippets) {\n\t\t\t\t\tconst snippets = data.snippets as Array<{\n\t\t\t\t\t\ttext: string\n\t\t\t\t\t\tscore: number\n\t\t\t\t\t}>\n\t\t\t\t\tcontent = snippets\n\t\t\t\t\t\t.map((s) => `${s.text} (Score: ${s.score})`)\n\t\t\t\t\t\t.join('\\n')\n\t\t\t\t} else {\n\t\t\t\t\tcontent = ''\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent,\n\t\t\t\t\turl: response.data.response.url,\n\t\t\t\t\t...(includeMeta && data.meta\n\t\t\t\t\t\t? { meta: data.meta as FetchResult['meta'] }\n\t\t\t\t\t\t: {}),\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Transform SDK errors into LLM-friendly messages\n\t\t\t\tif (error instanceof Expand.APIError) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Failed to fetch ${url}: ${error.message} (status: ${error.status})`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to fetch ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n}\n\n/**\n * Default Expand fetch tool instance.\n * Uses EXPAND_API_KEY environment variable for authentication.\n * Created lazily to avoid errors when importing the module without an API key.\n *\n * @example\n * ```typescript\n * import { expandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandFetchTool },\n * prompt: 'Summarize https://expand.ai',\n * })\n * ```\n */\nlet _expandFetchTool: ReturnType<typeof createExpandFetchTool> | undefined\n\nexport const expandFetchTool = new Proxy(\n\t{} as ReturnType<typeof createExpandFetchTool>,\n\t{\n\t\tget(_target, prop) {\n\t\t\tif (!_expandFetchTool) {\n\t\t\t\t_expandFetchTool = createExpandFetchTool()\n\t\t\t}\n\t\t\treturn (_expandFetchTool as Record<string | symbol, unknown>)[prop]\n\t\t},\n\t},\n)\n"]}
{"version":3,"sources":["../src/tool.ts"],"names":["z","Expand","tool"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ClientOptions } from '@expandai/sdk'\nimport Expand from '@expandai/sdk'\nimport { tool } from 'ai'\nimport { z } from 'zod'\n\n// Re-export ClientOptions for convenience\nexport type { ClientOptions }\n\n/**\n * Configuration options for the Expand fetch tool.\n */\nexport interface ExpandFetchToolOptions extends ClientOptions {\n\t/** Custom description for the tool (overrides default) */\n\tdescription?: string\n}\n\n// Zod schema for search params - matches MCP server\nconst searchParamsSchema = z.object({\n\tquery: z.string().describe('Query to find relevant content snippets'),\n\tminScore: z\n\t\t.number()\n\t\t.min(0)\n\t\t.max(1)\n\t\t.default(0.6)\n\t\t.describe('Minimum relevance score (0-1)'),\n\tmaxResults: z\n\t\t.number()\n\t\t.int()\n\t\t.min(1)\n\t\t.max(50)\n\t\t.default(5)\n\t\t.describe('Maximum snippets to return'),\n})\n\nconst inputSchema = z.object({\n\turl: z.string().url().describe('The URL to fetch content from'),\n\tincludeMeta: z\n\t\t.boolean()\n\t\t.default(false)\n\t\t.describe('Include page meta tags (title, description, Open Graph)'),\n\tsearch: searchParamsSchema\n\t\t.optional()\n\t\t.describe(\n\t\t\t'Semantic search. When provided, returns snippets instead of markdown.',\n\t\t),\n})\n\n/**\n * Create an Expand fetch tool for the Vercel AI SDK.\n *\n * The tool mirrors the MCP server's fetch tool, providing consistent behavior\n * across all Expand integrations.\n *\n * @param options - Configuration options (SDK client options + tool-specific options)\n * @returns A Vercel AI SDK tool that fetches and extracts web content\n *\n * @example\n * ```typescript\n * import { createExpandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n * import { openai } from '@ai-sdk/openai'\n *\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * })\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandTool },\n * prompt: 'What is on the Expand homepage?',\n * })\n * ```\n */\nexport function createExpandFetchTool(options?: ExpandFetchToolOptions) {\n\tconst apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY\n\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t'Expand API key is required. Provide it via options.apiKey or set EXPAND_API_KEY environment variable.',\n\t\t)\n\t}\n\n\t// Extract tool-specific options, pass rest to SDK client\n\tconst { description, ...clientOptions } = options ?? {}\n\n\t// Create Expand client with all client options\n\tconst client = new Expand({\n\t\t...clientOptions,\n\t\tapiKey,\n\t})\n\n\treturn tool({\n\t\tdescription:\n\t\t\tdescription ??\n\t\t\t'Fetch and extract content from any URL.\\n\\nReturns markdown by default. When `search` is provided, returns semantically relevant snippets instead.',\n\t\tinputSchema,\n\t\texecute: async (input) => {\n\t\t\tconst { url, includeMeta, search } = input\n\t\t\ttry {\n\t\t\t\tconst hasSearch = search !== undefined\n\t\t\t\tconst response = await client.fetch({\n\t\t\t\t\turl,\n\t\t\t\t\tselect: {\n\t\t\t\t\t\tmarkdown: !hasSearch,\n\t\t\t\t\t\tsnippets: hasSearch\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tquery: search.query,\n\t\t\t\t\t\t\t\t\tmaxSnippets: search.maxResults,\n\t\t\t\t\t\t\t\t\tminScore: search.minScore,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tmeta: includeMeta,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn response.data\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof Expand.APIError) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Failed to fetch ${url}: ${error.message} (status: ${error.status})`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to fetch ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n}\n\n/**\n * Default Expand fetch tool instance.\n * Uses EXPAND_API_KEY environment variable for authentication.\n * Created lazily to avoid errors when importing the module without an API key.\n *\n * @example\n * ```typescript\n * import { expandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandFetchTool },\n * prompt: 'Summarize https://expand.ai',\n * })\n * ```\n */\nlet _expandFetchTool: ReturnType<typeof createExpandFetchTool> | undefined\n\nexport const expandFetchTool = new Proxy(\n\t{} as ReturnType<typeof createExpandFetchTool>,\n\t{\n\t\tget(_target, prop) {\n\t\t\tif (!_expandFetchTool) {\n\t\t\t\t_expandFetchTool = createExpandFetchTool()\n\t\t\t}\n\t\t\treturn (_expandFetchTool as Record<string | symbol, unknown>)[prop]\n\t\t},\n\t},\n)\n"]}
import * as ai from 'ai';
import { ClientOptions } from '@expandai/sdk';
import Expand, { ClientOptions } from '@expandai/sdk';
export { ClientOptions } from '@expandai/sdk';
/**
* Snippets format configuration for semantic search within page content.
*/
interface SnippetsFormat {
/** Query to find relevant content snippets from the page */
query: string;
/** Maximum number of snippets to return (1-50, default: 5) */
maxSnippets?: number;
/** Target snippet size in characters (100-2000, default: 384) */
targetSnippetSize?: number;
}
/**
* Output format for the fetched content.
* - 'markdown': Return as cleaned markdown content
* - 'html': Return as raw HTML content
* - 'summary': Return AI-generated summary of the page content
* - SnippetsFormat: Return relevant content snippets matching a query
*/
type Format = 'markdown' | 'html' | 'summary' | SnippetsFormat;
/**
* Default configuration for the fetch tool.
* These values are used when the LLM doesn't specify them.
*/
interface FetchToolDefaults {
/** Default output format (default: 'markdown') */
format?: Format;
/** Whether to include page meta tags by default (default: true) */
includeMeta?: boolean;
/** Whether to scroll the full page to capture lazy-loaded content (default: false) */
scrollFullPage?: boolean;
}
/**
* Configuration options for the Expand fetch tool.

@@ -42,23 +11,4 @@ */

description?: string;
/** Default values for tool parameters */
defaults?: FetchToolDefaults;
}
/**
* Result returned by the fetch tool.
*/
interface FetchResult {
/** The fetched content in the requested format */
content: string;
/** The final URL after any redirects */
url: string;
/** Page metadata (title, description, etc.) if includeMeta was true */
meta?: {
title?: string | null;
description?: string | null;
canonicalUrl?: string | null;
language?: string | null;
charset?: string | null;
};
}
/**
* Create an Expand fetch tool for the Vercel AI SDK.

@@ -78,3 +28,2 @@ *

*
* // With default markdown format
* const expandTool = createExpandFetchTool({

@@ -84,11 +33,2 @@ * apiKey: process.env.EXPAND_API_KEY,

*
* // With custom defaults
* const expandTool = createExpandFetchTool({
* apiKey: process.env.EXPAND_API_KEY,
* defaults: {
* format: 'summary',
* includeMeta: false,
* },
* })
*
* const result = await generateText({

@@ -103,21 +43,19 @@ * model: openai('gpt-4'),

url: string;
format: "markdown" | "html" | "summary" | {
includeMeta: boolean;
search?: {
query: string;
maxSnippets?: number | undefined;
targetSnippetSize?: number | undefined;
};
includeMeta: boolean;
scrollFullPage: boolean;
}, FetchResult>;
minScore: number;
maxResults: number;
} | undefined;
}, Expand.FetchResponse.Data>;
declare const expandFetchTool: ai.Tool<{
url: string;
format: "markdown" | "html" | "summary" | {
includeMeta: boolean;
search?: {
query: string;
maxSnippets?: number | undefined;
targetSnippetSize?: number | undefined;
};
includeMeta: boolean;
scrollFullPage: boolean;
}, FetchResult>;
minScore: number;
maxResults: number;
} | undefined;
}, Expand.FetchResponse.Data>;
export { type ExpandFetchToolOptions, type FetchResult, type FetchToolDefaults, type Format, type SnippetsFormat, createExpandFetchTool, expandFetchTool };
export { type ExpandFetchToolOptions, createExpandFetchTool, expandFetchTool };
import * as ai from 'ai';
import { ClientOptions } from '@expandai/sdk';
import Expand, { ClientOptions } from '@expandai/sdk';
export { ClientOptions } from '@expandai/sdk';
/**
* Snippets format configuration for semantic search within page content.
*/
interface SnippetsFormat {
/** Query to find relevant content snippets from the page */
query: string;
/** Maximum number of snippets to return (1-50, default: 5) */
maxSnippets?: number;
/** Target snippet size in characters (100-2000, default: 384) */
targetSnippetSize?: number;
}
/**
* Output format for the fetched content.
* - 'markdown': Return as cleaned markdown content
* - 'html': Return as raw HTML content
* - 'summary': Return AI-generated summary of the page content
* - SnippetsFormat: Return relevant content snippets matching a query
*/
type Format = 'markdown' | 'html' | 'summary' | SnippetsFormat;
/**
* Default configuration for the fetch tool.
* These values are used when the LLM doesn't specify them.
*/
interface FetchToolDefaults {
/** Default output format (default: 'markdown') */
format?: Format;
/** Whether to include page meta tags by default (default: true) */
includeMeta?: boolean;
/** Whether to scroll the full page to capture lazy-loaded content (default: false) */
scrollFullPage?: boolean;
}
/**
* Configuration options for the Expand fetch tool.

@@ -42,23 +11,4 @@ */

description?: string;
/** Default values for tool parameters */
defaults?: FetchToolDefaults;
}
/**
* Result returned by the fetch tool.
*/
interface FetchResult {
/** The fetched content in the requested format */
content: string;
/** The final URL after any redirects */
url: string;
/** Page metadata (title, description, etc.) if includeMeta was true */
meta?: {
title?: string | null;
description?: string | null;
canonicalUrl?: string | null;
language?: string | null;
charset?: string | null;
};
}
/**
* Create an Expand fetch tool for the Vercel AI SDK.

@@ -78,3 +28,2 @@ *

*
* // With default markdown format
* const expandTool = createExpandFetchTool({

@@ -84,11 +33,2 @@ * apiKey: process.env.EXPAND_API_KEY,

*
* // With custom defaults
* const expandTool = createExpandFetchTool({
* apiKey: process.env.EXPAND_API_KEY,
* defaults: {
* format: 'summary',
* includeMeta: false,
* },
* })
*
* const result = await generateText({

@@ -103,21 +43,19 @@ * model: openai('gpt-4'),

url: string;
format: "markdown" | "html" | "summary" | {
includeMeta: boolean;
search?: {
query: string;
maxSnippets?: number | undefined;
targetSnippetSize?: number | undefined;
};
includeMeta: boolean;
scrollFullPage: boolean;
}, FetchResult>;
minScore: number;
maxResults: number;
} | undefined;
}, Expand.FetchResponse.Data>;
declare const expandFetchTool: ai.Tool<{
url: string;
format: "markdown" | "html" | "summary" | {
includeMeta: boolean;
search?: {
query: string;
maxSnippets?: number | undefined;
targetSnippetSize?: number | undefined;
};
includeMeta: boolean;
scrollFullPage: boolean;
}, FetchResult>;
minScore: number;
maxResults: number;
} | undefined;
}, Expand.FetchResponse.Data>;
export { type ExpandFetchToolOptions, type FetchResult, type FetchToolDefaults, type Format, type SnippetsFormat, createExpandFetchTool, expandFetchTool };
export { type ExpandFetchToolOptions, createExpandFetchTool, expandFetchTool };

@@ -6,13 +6,14 @@ import Expand from '@expandai/sdk';

// src/tool.ts
var snippetsFormatSchema = z.object({
query: z.string().describe("Query to find relevant content snippets from the page"),
maxSnippets: z.number().int().min(1).max(50).optional().describe("Maximum number of snippets to return (1-50, default: 5)"),
targetSnippetSize: z.number().int().min(100).max(2e3).optional().describe("Target snippet size in characters (100-2000, default: 384)")
var searchParamsSchema = z.object({
query: z.string().describe("Query to find relevant content snippets"),
minScore: z.number().min(0).max(1).default(0.6).describe("Minimum relevance score (0-1)"),
maxResults: z.number().int().min(1).max(50).default(5).describe("Maximum snippets to return")
});
var formatSchema = z.union([
z.literal("markdown").describe("Return as cleaned markdown content"),
z.literal("html").describe("Return as raw HTML content"),
z.literal("summary").describe("Return AI-generated summary of the page content"),
snippetsFormatSchema
]).describe("Output format for the fetched content");
var inputSchema = z.object({
url: z.string().url().describe("The URL to fetch content from"),
includeMeta: z.boolean().default(false).describe("Include page meta tags (title, description, Open Graph)"),
search: searchParamsSchema.optional().describe(
"Semantic search. When provided, returns snippets instead of markdown."
)
});
function createExpandFetchTool(options) {

@@ -25,3 +26,3 @@ const apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY;

}
const { description, defaults, ...clientOptions } = options ?? {};
const { description, ...clientOptions } = options ?? {};
const client = new Expand({

@@ -31,50 +32,22 @@ ...clientOptions,

});
const defaultFormat = defaults?.format ?? "markdown";
const defaultIncludeMeta = defaults?.includeMeta ?? true;
const defaultScrollFullPage = defaults?.scrollFullPage ?? false;
const inputSchema = z.object({
url: z.string().url().describe("The URL to fetch content from"),
format: formatSchema.default(defaultFormat),
includeMeta: z.boolean().default(defaultIncludeMeta).describe("Whether to include page meta tags in the response"),
scrollFullPage: z.boolean().default(defaultScrollFullPage).describe(
"Whether to scroll the full page to capture lazy-loaded content. Set to true for pages with infinite scroll or dynamic content loading."
)
});
return tool({
description: description ?? "Fetch and extract content from any URL.",
description: description ?? "Fetch and extract content from any URL.\n\nReturns markdown by default. When `search` is provided, returns semantically relevant snippets instead.",
inputSchema,
execute: async (input) => {
const { url, format, includeMeta, scrollFullPage } = input;
const { url, includeMeta, search } = input;
try {
const isSnippets = typeof format === "object" && "query" in format;
const hasSearch = search !== void 0;
const response = await client.fetch({
url,
select: {
markdown: format === "markdown",
html: format === "html",
summary: format === "summary",
snippets: isSnippets ? format : void 0,
markdown: !hasSearch,
snippets: hasSearch ? {
query: search.query,
maxSnippets: search.maxResults,
minScore: search.minScore
} : void 0,
meta: includeMeta
},
...scrollFullPage && { browserConfig: { scrollFullPage: true } }
}
});
const data = response.data;
let content;
if (format === "markdown" && data.markdown) {
content = data.markdown;
} else if (format === "html" && data.html) {
content = data.html;
} else if (format === "summary" && data.summary) {
content = data.summary;
} else if (isSnippets && data.snippets) {
const snippets = data.snippets;
content = snippets.map((s) => `${s.text} (Score: ${s.score})`).join("\n");
} else {
content = "";
}
return {
content,
url: response.data.response.url,
...includeMeta && data.meta ? { meta: data.meta } : {}
};
return response.data;
} catch (error) {

@@ -81,0 +54,0 @@ if (error instanceof Expand.APIError) {

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/tool.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ClientOptions } from '@expandai/sdk'\nimport Expand from '@expandai/sdk'\nimport { tool } from 'ai'\nimport { z } from 'zod'\n\n// Re-export ClientOptions for convenience\nexport type { ClientOptions }\n\n/**\n * Snippets format configuration for semantic search within page content.\n */\nexport interface SnippetsFormat {\n\t/** Query to find relevant content snippets from the page */\n\tquery: string\n\t/** Maximum number of snippets to return (1-50, default: 5) */\n\tmaxSnippets?: number\n\t/** Target snippet size in characters (100-2000, default: 384) */\n\ttargetSnippetSize?: number\n}\n\n/**\n * Output format for the fetched content.\n * - 'markdown': Return as cleaned markdown content\n * - 'html': Return as raw HTML content\n * - 'summary': Return AI-generated summary of the page content\n * - SnippetsFormat: Return relevant content snippets matching a query\n */\nexport type Format = 'markdown' | 'html' | 'summary' | SnippetsFormat\n\n/**\n * Default configuration for the fetch tool.\n * These values are used when the LLM doesn't specify them.\n */\nexport interface FetchToolDefaults {\n\t/** Default output format (default: 'markdown') */\n\tformat?: Format\n\t/** Whether to include page meta tags by default (default: true) */\n\tincludeMeta?: boolean\n\t/** Whether to scroll the full page to capture lazy-loaded content (default: false) */\n\tscrollFullPage?: boolean\n}\n\n/**\n * Configuration options for the Expand fetch tool.\n */\nexport interface ExpandFetchToolOptions extends ClientOptions {\n\t/** Custom description for the tool (overrides default) */\n\tdescription?: string\n\t/** Default values for tool parameters */\n\tdefaults?: FetchToolDefaults\n}\n\n/**\n * Result returned by the fetch tool.\n */\nexport interface FetchResult {\n\t/** The fetched content in the requested format */\n\tcontent: string\n\t/** The final URL after any redirects */\n\turl: string\n\t/** Page metadata (title, description, etc.) if includeMeta was true */\n\tmeta?: {\n\t\ttitle?: string | null\n\t\tdescription?: string | null\n\t\tcanonicalUrl?: string | null\n\t\tlanguage?: string | null\n\t\tcharset?: string | null\n\t}\n}\n\n// Zod schema for snippets format - matches MCP server\nconst snippetsFormatSchema = z.object({\n\tquery: z\n\t\t.string()\n\t\t.describe('Query to find relevant content snippets from the page'),\n\tmaxSnippets: z\n\t\t.number()\n\t\t.int()\n\t\t.min(1)\n\t\t.max(50)\n\t\t.optional()\n\t\t.describe('Maximum number of snippets to return (1-50, default: 5)'),\n\ttargetSnippetSize: z\n\t\t.number()\n\t\t.int()\n\t\t.min(100)\n\t\t.max(2000)\n\t\t.optional()\n\t\t.describe('Target snippet size in characters (100-2000, default: 384)'),\n})\n\n// Zod schema for format parameter - mirrors MCP server exactly\nconst formatSchema = z\n\t.union([\n\t\tz.literal('markdown').describe('Return as cleaned markdown content'),\n\t\tz.literal('html').describe('Return as raw HTML content'),\n\t\tz\n\t\t\t.literal('summary')\n\t\t\t.describe('Return AI-generated summary of the page content'),\n\t\tsnippetsFormatSchema,\n\t])\n\t.describe('Output format for the fetched content')\n\n/**\n * Create an Expand fetch tool for the Vercel AI SDK.\n *\n * The tool mirrors the MCP server's fetch tool, providing consistent behavior\n * across all Expand integrations.\n *\n * @param options - Configuration options (SDK client options + tool-specific options)\n * @returns A Vercel AI SDK tool that fetches and extracts web content\n *\n * @example\n * ```typescript\n * import { createExpandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n * import { openai } from '@ai-sdk/openai'\n *\n * // With default markdown format\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * })\n *\n * // With custom defaults\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * defaults: {\n * format: 'summary',\n * includeMeta: false,\n * },\n * })\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandTool },\n * prompt: 'What is on the Expand homepage?',\n * })\n * ```\n */\nexport function createExpandFetchTool(options?: ExpandFetchToolOptions) {\n\tconst apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY\n\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t'Expand API key is required. Provide it via options.apiKey or set EXPAND_API_KEY environment variable.',\n\t\t)\n\t}\n\n\t// Extract tool-specific options, pass rest to SDK client\n\tconst { description, defaults, ...clientOptions } = options ?? {}\n\n\t// Create Expand client with all client options\n\tconst client = new Expand({\n\t\t...clientOptions,\n\t\tapiKey,\n\t})\n\n\t// Apply defaults\n\tconst defaultFormat: Format = defaults?.format ?? 'markdown'\n\tconst defaultIncludeMeta = defaults?.includeMeta ?? true\n\tconst defaultScrollFullPage = defaults?.scrollFullPage ?? false\n\n\tconst inputSchema = z.object({\n\t\turl: z.string().url().describe('The URL to fetch content from'),\n\t\tformat: formatSchema.default(defaultFormat),\n\t\tincludeMeta: z\n\t\t\t.boolean()\n\t\t\t.default(defaultIncludeMeta)\n\t\t\t.describe('Whether to include page meta tags in the response'),\n\t\tscrollFullPage: z\n\t\t\t.boolean()\n\t\t\t.default(defaultScrollFullPage)\n\t\t\t.describe(\n\t\t\t\t'Whether to scroll the full page to capture lazy-loaded content. Set to true for pages with infinite scroll or dynamic content loading.',\n\t\t\t),\n\t})\n\n\ttype ToolInput = z.infer<typeof inputSchema>\n\n\treturn tool({\n\t\tdescription: description ?? 'Fetch and extract content from any URL.',\n\t\tinputSchema,\n\t\texecute: async (input: ToolInput): Promise<FetchResult> => {\n\t\t\tconst { url, format, includeMeta, scrollFullPage } = input\n\t\t\ttry {\n\t\t\t\t// Determine if format is snippets\n\t\t\t\tconst isSnippets = typeof format === 'object' && 'query' in format\n\n\t\t\t\tconst response = await client.fetch({\n\t\t\t\t\turl,\n\t\t\t\t\tselect: {\n\t\t\t\t\t\tmarkdown: format === 'markdown',\n\t\t\t\t\t\thtml: format === 'html',\n\t\t\t\t\t\tsummary: format === 'summary',\n\t\t\t\t\t\tsnippets: isSnippets ? format : undefined,\n\t\t\t\t\t\tmeta: includeMeta,\n\t\t\t\t\t},\n\t\t\t\t\t...(scrollFullPage && { browserConfig: { scrollFullPage: true } }),\n\t\t\t\t})\n\n\t\t\t\t// Build content based on format\n\t\t\t\tconst data = response.data as unknown as Record<string, unknown>\n\t\t\t\tlet content: string\n\n\t\t\t\tif (format === 'markdown' && data.markdown) {\n\t\t\t\t\tcontent = data.markdown as string\n\t\t\t\t} else if (format === 'html' && data.html) {\n\t\t\t\t\tcontent = data.html as string\n\t\t\t\t} else if (format === 'summary' && data.summary) {\n\t\t\t\t\tcontent = data.summary as string\n\t\t\t\t} else if (isSnippets && data.snippets) {\n\t\t\t\t\tconst snippets = data.snippets as Array<{\n\t\t\t\t\t\ttext: string\n\t\t\t\t\t\tscore: number\n\t\t\t\t\t}>\n\t\t\t\t\tcontent = snippets\n\t\t\t\t\t\t.map((s) => `${s.text} (Score: ${s.score})`)\n\t\t\t\t\t\t.join('\\n')\n\t\t\t\t} else {\n\t\t\t\t\tcontent = ''\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent,\n\t\t\t\t\turl: response.data.response.url,\n\t\t\t\t\t...(includeMeta && data.meta\n\t\t\t\t\t\t? { meta: data.meta as FetchResult['meta'] }\n\t\t\t\t\t\t: {}),\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Transform SDK errors into LLM-friendly messages\n\t\t\t\tif (error instanceof Expand.APIError) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Failed to fetch ${url}: ${error.message} (status: ${error.status})`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to fetch ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n}\n\n/**\n * Default Expand fetch tool instance.\n * Uses EXPAND_API_KEY environment variable for authentication.\n * Created lazily to avoid errors when importing the module without an API key.\n *\n * @example\n * ```typescript\n * import { expandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandFetchTool },\n * prompt: 'Summarize https://expand.ai',\n * })\n * ```\n */\nlet _expandFetchTool: ReturnType<typeof createExpandFetchTool> | undefined\n\nexport const expandFetchTool = new Proxy(\n\t{} as ReturnType<typeof createExpandFetchTool>,\n\t{\n\t\tget(_target, prop) {\n\t\t\tif (!_expandFetchTool) {\n\t\t\t\t_expandFetchTool = createExpandFetchTool()\n\t\t\t}\n\t\t\treturn (_expandFetchTool as Record<string | symbol, unknown>)[prop]\n\t\t},\n\t},\n)\n"]}
{"version":3,"sources":["../src/tool.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ClientOptions } from '@expandai/sdk'\nimport Expand from '@expandai/sdk'\nimport { tool } from 'ai'\nimport { z } from 'zod'\n\n// Re-export ClientOptions for convenience\nexport type { ClientOptions }\n\n/**\n * Configuration options for the Expand fetch tool.\n */\nexport interface ExpandFetchToolOptions extends ClientOptions {\n\t/** Custom description for the tool (overrides default) */\n\tdescription?: string\n}\n\n// Zod schema for search params - matches MCP server\nconst searchParamsSchema = z.object({\n\tquery: z.string().describe('Query to find relevant content snippets'),\n\tminScore: z\n\t\t.number()\n\t\t.min(0)\n\t\t.max(1)\n\t\t.default(0.6)\n\t\t.describe('Minimum relevance score (0-1)'),\n\tmaxResults: z\n\t\t.number()\n\t\t.int()\n\t\t.min(1)\n\t\t.max(50)\n\t\t.default(5)\n\t\t.describe('Maximum snippets to return'),\n})\n\nconst inputSchema = z.object({\n\turl: z.string().url().describe('The URL to fetch content from'),\n\tincludeMeta: z\n\t\t.boolean()\n\t\t.default(false)\n\t\t.describe('Include page meta tags (title, description, Open Graph)'),\n\tsearch: searchParamsSchema\n\t\t.optional()\n\t\t.describe(\n\t\t\t'Semantic search. When provided, returns snippets instead of markdown.',\n\t\t),\n})\n\n/**\n * Create an Expand fetch tool for the Vercel AI SDK.\n *\n * The tool mirrors the MCP server's fetch tool, providing consistent behavior\n * across all Expand integrations.\n *\n * @param options - Configuration options (SDK client options + tool-specific options)\n * @returns A Vercel AI SDK tool that fetches and extracts web content\n *\n * @example\n * ```typescript\n * import { createExpandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n * import { openai } from '@ai-sdk/openai'\n *\n * const expandTool = createExpandFetchTool({\n * apiKey: process.env.EXPAND_API_KEY,\n * })\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandTool },\n * prompt: 'What is on the Expand homepage?',\n * })\n * ```\n */\nexport function createExpandFetchTool(options?: ExpandFetchToolOptions) {\n\tconst apiKey = options?.apiKey ?? process.env.EXPAND_API_KEY\n\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t'Expand API key is required. Provide it via options.apiKey or set EXPAND_API_KEY environment variable.',\n\t\t)\n\t}\n\n\t// Extract tool-specific options, pass rest to SDK client\n\tconst { description, ...clientOptions } = options ?? {}\n\n\t// Create Expand client with all client options\n\tconst client = new Expand({\n\t\t...clientOptions,\n\t\tapiKey,\n\t})\n\n\treturn tool({\n\t\tdescription:\n\t\t\tdescription ??\n\t\t\t'Fetch and extract content from any URL.\\n\\nReturns markdown by default. When `search` is provided, returns semantically relevant snippets instead.',\n\t\tinputSchema,\n\t\texecute: async (input) => {\n\t\t\tconst { url, includeMeta, search } = input\n\t\t\ttry {\n\t\t\t\tconst hasSearch = search !== undefined\n\t\t\t\tconst response = await client.fetch({\n\t\t\t\t\turl,\n\t\t\t\t\tselect: {\n\t\t\t\t\t\tmarkdown: !hasSearch,\n\t\t\t\t\t\tsnippets: hasSearch\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tquery: search.query,\n\t\t\t\t\t\t\t\t\tmaxSnippets: search.maxResults,\n\t\t\t\t\t\t\t\t\tminScore: search.minScore,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tmeta: includeMeta,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn response.data\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof Expand.APIError) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Failed to fetch ${url}: ${error.message} (status: ${error.status})`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to fetch ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n}\n\n/**\n * Default Expand fetch tool instance.\n * Uses EXPAND_API_KEY environment variable for authentication.\n * Created lazily to avoid errors when importing the module without an API key.\n *\n * @example\n * ```typescript\n * import { expandFetchTool } from '@expandai/ai'\n * import { generateText } from 'ai'\n *\n * const result = await generateText({\n * model: openai('gpt-4'),\n * tools: { fetch: expandFetchTool },\n * prompt: 'Summarize https://expand.ai',\n * })\n * ```\n */\nlet _expandFetchTool: ReturnType<typeof createExpandFetchTool> | undefined\n\nexport const expandFetchTool = new Proxy(\n\t{} as ReturnType<typeof createExpandFetchTool>,\n\t{\n\t\tget(_target, prop) {\n\t\t\tif (!_expandFetchTool) {\n\t\t\t\t_expandFetchTool = createExpandFetchTool()\n\t\t\t}\n\t\t\treturn (_expandFetchTool as Record<string | symbol, unknown>)[prop]\n\t\t},\n\t},\n)\n"]}
{
"name": "@expandai/ai",
"version": "0.1.0",
"version": "0.1.1",
"description": "Vercel AI SDK integration for expand.ai - fetch and extract content from any URL",

@@ -59,14 +59,14 @@ "type": "module",

"dependencies": {
"@expandai/sdk": "^0.5.1"
"@expandai/sdk": "^0.16.0"
},
"devDependencies": {
"@biomejs/biome": "^2.1.1",
"@biomejs/biome": "^2.4.4",
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.29.8",
"@types/node": "^22.10.5",
"ai": "^6.0.0",
"@types/node": "^25.3.0",
"ai": "^6.0.97",
"tsup": "^8.5.1",
"typescript": "^5.7.3",
"vitest": "^2.1.8",
"zod": "^3.25.76"
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"zod": "^4.3.6"
},

@@ -73,0 +73,0 @@ "scripts": {

@@ -29,6 +29,2 @@ /**

expandFetchTool,
type FetchResult,
type FetchToolDefaults,
type Format,
type SnippetsFormat,
} from './tool.js'

@@ -10,36 +10,2 @@ import type { ClientOptions } from '@expandai/sdk'

/**
* Snippets format configuration for semantic search within page content.
*/
export interface SnippetsFormat {
/** Query to find relevant content snippets from the page */
query: string
/** Maximum number of snippets to return (1-50, default: 5) */
maxSnippets?: number
/** Target snippet size in characters (100-2000, default: 384) */
targetSnippetSize?: number
}
/**
* Output format for the fetched content.
* - 'markdown': Return as cleaned markdown content
* - 'html': Return as raw HTML content
* - 'summary': Return AI-generated summary of the page content
* - SnippetsFormat: Return relevant content snippets matching a query
*/
export type Format = 'markdown' | 'html' | 'summary' | SnippetsFormat
/**
* Default configuration for the fetch tool.
* These values are used when the LLM doesn't specify them.
*/
export interface FetchToolDefaults {
/** Default output format (default: 'markdown') */
format?: Format
/** Whether to include page meta tags by default (default: true) */
includeMeta?: boolean
/** Whether to scroll the full page to capture lazy-loaded content (default: false) */
scrollFullPage?: boolean
}
/**
* Configuration options for the Expand fetch tool.

@@ -50,57 +16,35 @@ */

description?: string
/** Default values for tool parameters */
defaults?: FetchToolDefaults
}
/**
* Result returned by the fetch tool.
*/
export interface FetchResult {
/** The fetched content in the requested format */
content: string
/** The final URL after any redirects */
url: string
/** Page metadata (title, description, etc.) if includeMeta was true */
meta?: {
title?: string | null
description?: string | null
canonicalUrl?: string | null
language?: string | null
charset?: string | null
}
}
// Zod schema for snippets format - matches MCP server
const snippetsFormatSchema = z.object({
query: z
.string()
.describe('Query to find relevant content snippets from the page'),
maxSnippets: z
// Zod schema for search params - matches MCP server
const searchParamsSchema = z.object({
query: z.string().describe('Query to find relevant content snippets'),
minScore: z
.number()
.min(0)
.max(1)
.default(0.6)
.describe('Minimum relevance score (0-1)'),
maxResults: z
.number()
.int()
.min(1)
.max(50)
.default(5)
.describe('Maximum snippets to return'),
})
const inputSchema = z.object({
url: z.string().url().describe('The URL to fetch content from'),
includeMeta: z
.boolean()
.default(false)
.describe('Include page meta tags (title, description, Open Graph)'),
search: searchParamsSchema
.optional()
.describe('Maximum number of snippets to return (1-50, default: 5)'),
targetSnippetSize: z
.number()
.int()
.min(100)
.max(2000)
.optional()
.describe('Target snippet size in characters (100-2000, default: 384)'),
.describe(
'Semantic search. When provided, returns snippets instead of markdown.',
),
})
// Zod schema for format parameter - mirrors MCP server exactly
const formatSchema = z
.union([
z.literal('markdown').describe('Return as cleaned markdown content'),
z.literal('html').describe('Return as raw HTML content'),
z
.literal('summary')
.describe('Return AI-generated summary of the page content'),
snippetsFormatSchema,
])
.describe('Output format for the fetched content')
/**

@@ -121,3 +65,2 @@ * Create an Expand fetch tool for the Vercel AI SDK.

*
* // With default markdown format
* const expandTool = createExpandFetchTool({

@@ -127,11 +70,2 @@ * apiKey: process.env.EXPAND_API_KEY,

*
* // With custom defaults
* const expandTool = createExpandFetchTool({
* apiKey: process.env.EXPAND_API_KEY,
* defaults: {
* format: 'summary',
* includeMeta: false,
* },
* })
*
* const result = await generateText({

@@ -154,3 +88,3 @@ * model: openai('gpt-4'),

// Extract tool-specific options, pass rest to SDK client
const { description, defaults, ...clientOptions } = options ?? {}
const { description, ...clientOptions } = options ?? {}

@@ -163,76 +97,27 @@ // Create Expand client with all client options

// Apply defaults
const defaultFormat: Format = defaults?.format ?? 'markdown'
const defaultIncludeMeta = defaults?.includeMeta ?? true
const defaultScrollFullPage = defaults?.scrollFullPage ?? false
const inputSchema = z.object({
url: z.string().url().describe('The URL to fetch content from'),
format: formatSchema.default(defaultFormat),
includeMeta: z
.boolean()
.default(defaultIncludeMeta)
.describe('Whether to include page meta tags in the response'),
scrollFullPage: z
.boolean()
.default(defaultScrollFullPage)
.describe(
'Whether to scroll the full page to capture lazy-loaded content. Set to true for pages with infinite scroll or dynamic content loading.',
),
})
type ToolInput = z.infer<typeof inputSchema>
return tool({
description: description ?? 'Fetch and extract content from any URL.',
description:
description ??
'Fetch and extract content from any URL.\n\nReturns markdown by default. When `search` is provided, returns semantically relevant snippets instead.',
inputSchema,
execute: async (input: ToolInput): Promise<FetchResult> => {
const { url, format, includeMeta, scrollFullPage } = input
execute: async (input) => {
const { url, includeMeta, search } = input
try {
// Determine if format is snippets
const isSnippets = typeof format === 'object' && 'query' in format
const hasSearch = search !== undefined
const response = await client.fetch({
url,
select: {
markdown: format === 'markdown',
html: format === 'html',
summary: format === 'summary',
snippets: isSnippets ? format : undefined,
markdown: !hasSearch,
snippets: hasSearch
? {
query: search.query,
maxSnippets: search.maxResults,
minScore: search.minScore,
}
: undefined,
meta: includeMeta,
},
...(scrollFullPage && { browserConfig: { scrollFullPage: true } }),
})
// Build content based on format
const data = response.data as unknown as Record<string, unknown>
let content: string
if (format === 'markdown' && data.markdown) {
content = data.markdown as string
} else if (format === 'html' && data.html) {
content = data.html as string
} else if (format === 'summary' && data.summary) {
content = data.summary as string
} else if (isSnippets && data.snippets) {
const snippets = data.snippets as Array<{
text: string
score: number
}>
content = snippets
.map((s) => `${s.text} (Score: ${s.score})`)
.join('\n')
} else {
content = ''
}
return {
content,
url: response.data.response.url,
...(includeMeta && data.meta
? { meta: data.meta as FetchResult['meta'] }
: {}),
}
return response.data
} catch (error) {
// Transform SDK errors into LLM-friendly messages
if (error instanceof Expand.APIError) {

@@ -239,0 +124,0 @@ throw new Error(