From c8e7eb5ca66b01571c8540609138e5d500fcaea1 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Mon, 24 Mar 2025 00:46:55 +0800 Subject: [PATCH] feat: add MCP, arxiv, and duck-duck-go tools --- legacy/docs/tools/arxiv.mdx | 39 +++ legacy/docs/tools/duck-duck-go.mdx | 38 +++ legacy/docs/tools/mcp.mdx | 85 ++++++ legacy/examples/ai-sdk/bin/mcp-filesystem.ts | 44 ++++ legacy/packages/arxiv/package.json | 47 ++++ legacy/packages/arxiv/readme.md | 24 ++ legacy/packages/arxiv/src/arxiv-client.ts | 243 ++++++++++++++++++ legacy/packages/arxiv/src/index.ts | 1 + legacy/packages/arxiv/src/utils.ts | 30 +++ legacy/packages/arxiv/tsconfig.json | 5 + legacy/packages/duck-duck-go/package.json | 48 ++++ legacy/packages/duck-duck-go/readme.md | 24 ++ .../duck-duck-go/src/duck-duck-go-client.ts | 78 ++++++ legacy/packages/duck-duck-go/src/index.ts | 1 + legacy/packages/duck-duck-go/src/paginate.ts | 34 +++ legacy/packages/duck-duck-go/tsconfig.json | 5 + legacy/packages/mcp/package.json | 46 ++++ legacy/packages/mcp/readme.md | 24 ++ legacy/packages/mcp/src/index.ts | 2 + legacy/packages/mcp/src/mcp-tools.ts | 216 ++++++++++++++++ legacy/packages/mcp/src/paginate.ts | 34 +++ legacy/packages/mcp/src/types.ts | 53 ++++ legacy/packages/mcp/tsconfig.json | 5 + 23 files changed, 1126 insertions(+) create mode 100644 legacy/docs/tools/arxiv.mdx create mode 100644 legacy/docs/tools/duck-duck-go.mdx create mode 100644 legacy/docs/tools/mcp.mdx create mode 100644 legacy/examples/ai-sdk/bin/mcp-filesystem.ts create mode 100644 legacy/packages/arxiv/package.json create mode 100644 legacy/packages/arxiv/readme.md create mode 100644 legacy/packages/arxiv/src/arxiv-client.ts create mode 100644 legacy/packages/arxiv/src/index.ts create mode 100644 legacy/packages/arxiv/src/utils.ts create mode 100644 legacy/packages/arxiv/tsconfig.json create mode 100644 legacy/packages/duck-duck-go/package.json create mode 100644 legacy/packages/duck-duck-go/readme.md create mode 100644 legacy/packages/duck-duck-go/src/duck-duck-go-client.ts create mode 100644 legacy/packages/duck-duck-go/src/index.ts create mode 100644 legacy/packages/duck-duck-go/src/paginate.ts create mode 100644 legacy/packages/duck-duck-go/tsconfig.json create mode 100644 legacy/packages/mcp/package.json create mode 100644 legacy/packages/mcp/readme.md create mode 100644 legacy/packages/mcp/src/index.ts create mode 100644 legacy/packages/mcp/src/mcp-tools.ts create mode 100644 legacy/packages/mcp/src/paginate.ts create mode 100644 legacy/packages/mcp/src/types.ts create mode 100644 legacy/packages/mcp/tsconfig.json diff --git a/legacy/docs/tools/arxiv.mdx b/legacy/docs/tools/arxiv.mdx new file mode 100644 index 00000000..a661307d --- /dev/null +++ b/legacy/docs/tools/arxiv.mdx @@ -0,0 +1,39 @@ +--- +title: ArXiv +description: Search for research articles. +--- + +- package: `@agentic/arxiv` +- exports: `class ArXivClient`, `namespace arxiv` +- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/arxiv/src/arxiv-client.ts) +- [arxiv api docs](https://info.arxiv.org/help/api/index.html) + +## Install + + +```bash npm +npm install @agentic/arxiv +``` + +```bash yarn +yarn add @agentic/arxiv +``` + +```bash pnpm +pnpm add @agentic/arxiv +``` + + + +## Usage + +```ts +import { ArXivClient } from '@agentic/arxiv' + +// No API is required to use the ArXiv API +const arxiv = new ArXivClient() +const results = await arxiv.search({ + query: 'machine learning', + maxResults: 10 +}) +``` diff --git a/legacy/docs/tools/duck-duck-go.mdx b/legacy/docs/tools/duck-duck-go.mdx new file mode 100644 index 00000000..84e90d55 --- /dev/null +++ b/legacy/docs/tools/duck-duck-go.mdx @@ -0,0 +1,38 @@ +--- +title: DuckDuckGo +description: Search for research articles. +--- + +- package: `@agentic/duck-duck-go` +- exports: `class DuckDuckGoClient`, `namespace duckduckgo` +- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/duck-duck-go/src/duck-duck-go-client.ts) +- [Duck Duck Go api docs](https://api.duckduckgo.com) + +## Install + + +```bash npm +npm install @agentic/duck-duck-go +``` + +```bash yarn +yarn add @agentic/duck-duck-go +``` + +```bash pnpm +pnpm add @agentic/duck-duck-go +``` + + + +## Usage + +```ts +import { DuckDuckGoClient } from '@agentic/duck-duck-go' + +// No API is required to use the DuckDuckGo API +const duckDuckGo = new DuckDuckGoClient() +const results = await duckDuckGo.search({ + query: 'latest news about AI' +}) +``` diff --git a/legacy/docs/tools/mcp.mdx b/legacy/docs/tools/mcp.mdx new file mode 100644 index 00000000..fb84c972 --- /dev/null +++ b/legacy/docs/tools/mcp.mdx @@ -0,0 +1,85 @@ +--- +title: MCP Tools +description: Agentic adapter for accessing tools defined by Model Context Protocol (MCP) servers. +--- + +- package: `@agentic/mcp` +- exports: `class McpTools`, `createMcpTools` +- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/mcp/src/mcp-tools.ts) +- [MCP docs](https://modelcontextprotocol.io) + +## Install + + +```bash npm +npm install @agentic/mcp +``` + +```bash yarn +yarn add @agentic/mcp +``` + +```bash pnpm +pnpm add @agentic/mcp +``` + + + +## Usage + +```ts +import 'dotenv/config' + +import { createAISDKTools } from '@agentic/ai-sdk' +import { createMcpTools } from '@agentic/mcp' +import { openai } from '@ai-sdk/openai' +import { generateText } from 'ai' + +async function main() { + // Create an MCP tools provider, which will start a local MCP server process + // and use the stdio transport to communicate with it. + const mcpTools = await createMcpTools({ + name: 'agentic-mcp-filesystem', + serverProcess: { + command: 'npx', + args: [ + '-y', + // This example uses a built-in example MCP server from Anthropic, which + // provides a set of tools to access the local filesystem. + '@modelcontextprotocol/server-filesystem', + // Allow the MCP server to access the current working directory. + process.cwd() + // Feel free to add additional directories the tool should have access to. + ] + } + }) + + const result = await generateText({ + model: openai('gpt-4o-mini'), + tools: createAISDKTools(mcpTools), + toolChoice: 'required', + temperature: 0, + system: 'You are a helpful assistant. Be as concise as possible.', + prompt: 'What files are in the current directory?' + }) + + console.log(result.toolResults[0]) +} + +await main() +``` + +### createMcpTools + +`createMcpTools` creates a new `McpTools` instance by starting or connecting to an MCP server. + +You must provide either an existing `transport`, an existing `serverUrl`, or a +`serverProcess` to spawn. + +All tools within the `McpTools` instance will be namespaced under the given `name`. + +### JSON Schema + +Note that `McpTools` uses JSON Schemas for toll input parameters, whereas most built-in tools use Zod schemas. This is important because some AI frameworks don't support JSON Schemas. + +Currently, Mastra, Dexter, and xsAI don't support JSON Schema input parameters, so they won't work with `McpTools`. diff --git a/legacy/examples/ai-sdk/bin/mcp-filesystem.ts b/legacy/examples/ai-sdk/bin/mcp-filesystem.ts new file mode 100644 index 00000000..a1f66f81 --- /dev/null +++ b/legacy/examples/ai-sdk/bin/mcp-filesystem.ts @@ -0,0 +1,44 @@ +import 'dotenv/config' + +import { createAISDKTools } from '@agentic/ai-sdk' +import { createMcpTools } from '@agentic/mcp' +import { openai } from '@ai-sdk/openai' +import { generateText } from 'ai' +import { gracefulExit } from 'exit-hook' + +async function main() { + // Create an MCP tools provider, which will start a local MCP server process + // and use the stdio transport to communicate with it. + const mcpTools = await createMcpTools({ + name: 'agentic-mcp-filesystem', + serverProcess: { + command: 'npx', + args: [ + '-y', + '@modelcontextprotocol/server-filesystem', + // Allow the MCP server to access the current working directory. + process.cwd() + // Feel free to add additional directories the tool should have access to. + ] + } + }) + + const result = await generateText({ + model: openai('gpt-4o-mini'), + tools: createAISDKTools(mcpTools), + toolChoice: 'required', + temperature: 0, + system: 'You are a helpful assistant. Be as concise as possible.', + prompt: 'What files are in the current directory?' + }) + + console.log(result.toolResults[0]?.result || JSON.stringify(result, null, 2)) +} + +try { + await main() + gracefulExit(0) +} catch (err) { + console.error(err) + gracefulExit(1) +} diff --git a/legacy/packages/arxiv/package.json b/legacy/packages/arxiv/package.json new file mode 100644 index 00000000..36e19aeb --- /dev/null +++ b/legacy/packages/arxiv/package.json @@ -0,0 +1,47 @@ +{ + "name": "@agentic/arxiv", + "version": "0.1.0", + "description": "Agentic SDK for Arxiv.", + "author": "Travis Fischer ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/transitive-bullshit/agentic.git" + }, + "type": "module", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "del dist", + "test": "run-s test:*", + "test:lint": "eslint .", + "test:typecheck": "tsc --noEmit" + }, + "dependencies": { + "@agentic/core": "workspace:*", + "fast-xml-parser": "catalog:", + "ky": "catalog:" + }, + "peerDependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@agentic/tsconfig": "workspace:*" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/legacy/packages/arxiv/readme.md b/legacy/packages/arxiv/readme.md new file mode 100644 index 00000000..38781f32 --- /dev/null +++ b/legacy/packages/arxiv/readme.md @@ -0,0 +1,24 @@ +

+ + Agentic + +

+ +

+ AI agent stdlib that works with any LLM and TypeScript AI SDK. +

+ +

+ Build Status + NPM + MIT License + Prettier Code Formatting +

+ +# Agentic + +**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.** + +## License + +MIT © [Travis Fischer](https://x.com/transitive_bs) diff --git a/legacy/packages/arxiv/src/arxiv-client.ts b/legacy/packages/arxiv/src/arxiv-client.ts new file mode 100644 index 00000000..da829967 --- /dev/null +++ b/legacy/packages/arxiv/src/arxiv-client.ts @@ -0,0 +1,243 @@ +import { + aiFunction, + AIFunctionsProvider, + pruneEmpty, + sanitizeSearchParams +} from '@agentic/core' +import { XMLParser } from 'fast-xml-parser' +import defaultKy, { type KyInstance } from 'ky' +import { z } from 'zod' + +import { castArray, getProp } from './utils' + +export namespace arxiv { + export const API_BASE_URL = 'https://export.arxiv.org/api' + + export const SortType = { + RELEVANCE: 'relevance', + LAST_UPDATED_DATE: 'lastUpdatedDate', + SUBMITTED_DATE: 'submittedDate' + } as const + + export const SortOrder = { + ASCENDING: 'ascending', + DESCENDING: 'descending' + } as const + + export const FilterType = { + ALL: 'all', + TITLE: 'title', + AUTHOR: 'author', + ABSTRACT: 'abstract', + COMMENT: 'comment', + JOURNAL_REFERENCE: 'journal_reference', + SUBJECT_CATEGORY: 'subject_category', + REPORT_NUMBER: 'report_number' + } as const + + export type ValueOf> = T[keyof T] + export const FilterTypeMapping: Record, string> = { + all: 'all', + title: 'ti', + author: 'au', + abstract: 'abs', + comment: 'co', + journal_reference: 'jr', + subject_category: 'cat', + report_number: 'rn' + } + + export const Separators = { + AND: '+AND+', + OR: '+OR+', + ANDNOT: '+ANDNOT+' + } as const + + export interface ArXivResponse { + totalResults: number + startIndex: number + itemsPerPage: number + entries: { + id: string + title: string + summary: string + published: string + updated: string + authors: { name: string; affiliation: string[] }[] + doi: string + comment: string + journalReference: string + primaryCategory: string + categories: string[] + links: string[] + }[] + } + + export const extractId = (value: string) => + value + .replace('https://arxiv.org/abs/', '') + .replace('https://arxiv.org/pdf/', '') + .replace(/v\d$/, '') + + const EntrySchema = z.object({ + field: z.nativeEnum(FilterType).default(FilterType.ALL), + value: z.string().min(1) + }) + + export const SearchParamsSchema = z + .object({ + ids: z.array(z.string().min(1)).optional(), + searchQuery: z + .union([ + z.string(), + z.object({ + include: z + .array(EntrySchema) + .nonempty() + .describe('Filters to include results.'), + exclude: z + .array(EntrySchema) + .optional() + .describe('Filters to exclude results.') + }) + ]) + .optional(), + start: z.number().int().min(0).default(0), + maxResults: z.number().int().min(1).max(100).default(5) + }) + .describe('Sorting by date is not supported.') + export type SearchParams = z.infer +} + +/** + * Lightweight wrapper around ArXiv for academic / scholarly research articles. + * + * @see https://arxiv.org + */ +export class ArXivClient extends AIFunctionsProvider { + protected readonly ky: KyInstance + protected readonly apiBaseUrl: string + + constructor({ + apiBaseUrl = arxiv.API_BASE_URL, + ky = defaultKy + }: { + apiKey?: string + apiBaseUrl?: string + ky?: KyInstance + }) { + super() + + this.apiBaseUrl = apiBaseUrl + + this.ky = ky.extend({ + prefixUrl: this.apiBaseUrl + }) + } + + /** + * Searches for research articles published on arXiv. + */ + @aiFunction({ + name: 'arxiv_search', + description: 'Searches for research articles published on arXiv.', + inputSchema: arxiv.SearchParamsSchema + }) + async search(queryOrOpts: string | arxiv.SearchParams) { + const opts = + typeof queryOrOpts === 'string' + ? ({ searchQuery: queryOrOpts } as arxiv.SearchParams) + : queryOrOpts + + if (!opts.ids?.length && !opts.searchQuery) { + throw new Error( + `The 'searchQuery' property must be non-empty if the 'ids' property is not provided.` + ) + } + + const searchParams = sanitizeSearchParams({ + start: opts.start, + max_results: opts.maxResults, + id_list: opts.ids?.map(arxiv.extractId), + search_query: opts.searchQuery + ? typeof opts.searchQuery === 'string' + ? opts.searchQuery + : [ + opts.searchQuery.include + .map( + (tag) => `${arxiv.FilterTypeMapping[tag.field]}:${tag.value}` + ) + .join(arxiv.Separators.AND), + (opts.searchQuery.exclude ?? []) + .map( + (tag) => `${arxiv.FilterTypeMapping[tag.field]}:${tag.value}` + ) + .join(arxiv.Separators.ANDNOT) + ] + .filter(Boolean) + .join(arxiv.Separators.ANDNOT) + : undefined, + sortBy: arxiv.SortType.RELEVANCE, + sortOrder: arxiv.SortOrder.DESCENDING + }) + + const responseText = await this.ky.get('query', { searchParams }).text() + + const parser = new XMLParser({ + allowBooleanAttributes: true, + alwaysCreateTextNode: false, + attributeNamePrefix: '@_', + attributesGroupName: false, + cdataPropName: '#cdata', + ignoreAttributes: true, + numberParseOptions: { hex: false, leadingZeros: true }, + parseAttributeValue: false, + parseTagValue: true, + preserveOrder: false, + removeNSPrefix: true, + textNodeName: '#text', + trimValues: true, + ignoreDeclaration: true + }) + + const parsedData = parser.parse(responseText) + + let entries: Record[] = getProp( + parsedData, + ['feed', 'entry'], + [] + ) + entries = castArray(entries) + + return { + totalResults: Math.max( + getProp(parsedData, ['feed', 'totalResults'], 0), + entries.length + ), + startIndex: getProp(parsedData, ['feed', 'startIndex'], 0), + itemsPerPage: getProp(parsedData, ['feed', 'itemsPerPage'], 0), + entries: entries.map((entry) => + pruneEmpty({ + id: arxiv.extractId(entry.id), + url: entry.id, + title: entry.title, + summary: entry.summary, + published: entry.published, + updated: entry.updated, + authors: castArray(entry.author) + .filter(Boolean) + .map((author: any) => ({ + name: author.name, + affiliation: castArray(author.affiliation ?? []) + })), + doi: entry.doi, + comment: entry.comment, + journalReference: entry.journal_ref, + primaryCategory: entry.primary_category, + categories: castArray(entry.category).filter(Boolean), + links: castArray(entry.link).filter(Boolean) + }) + ) + } + } +} diff --git a/legacy/packages/arxiv/src/index.ts b/legacy/packages/arxiv/src/index.ts new file mode 100644 index 00000000..1a4862f8 --- /dev/null +++ b/legacy/packages/arxiv/src/index.ts @@ -0,0 +1 @@ +export * from './arxiv-client' diff --git a/legacy/packages/arxiv/src/utils.ts b/legacy/packages/arxiv/src/utils.ts new file mode 100644 index 00000000..5b671328 --- /dev/null +++ b/legacy/packages/arxiv/src/utils.ts @@ -0,0 +1,30 @@ +export function hasProp( + target: T | undefined, + key: keyof T +): key is keyof T { + return Boolean(target) && Object.prototype.hasOwnProperty.call(target, key) +} + +export function getProp( + target: unknown, + paths: readonly (keyof any)[], + defaultValue: any = undefined +) { + let value: any = target + if (!value) { + return undefined + } + + for (const key of paths) { + if (!hasProp(value, key)) { + return defaultValue + } + value = value[key] + } + return value +} + +export function castArray(arr: T) { + const result = Array.isArray(arr) ? arr : [arr] + return result as T extends unknown[] ? T : [T] +} diff --git a/legacy/packages/arxiv/tsconfig.json b/legacy/packages/arxiv/tsconfig.json new file mode 100644 index 00000000..6c8d720c --- /dev/null +++ b/legacy/packages/arxiv/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@agentic/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/legacy/packages/duck-duck-go/package.json b/legacy/packages/duck-duck-go/package.json new file mode 100644 index 00000000..82e2b7ef --- /dev/null +++ b/legacy/packages/duck-duck-go/package.json @@ -0,0 +1,48 @@ +{ + "name": "@agentic/duck-duck-go", + "version": "7.5.3", + "description": "Agentic SDK for DuckDuckGo.", + "author": "Travis Fischer ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/transitive-bullshit/agentic.git" + }, + "type": "module", + "source": "./src/index.ts", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "del dist", + "test": "run-s test:*", + "test:lint": "eslint .", + "test:typecheck": "tsc --noEmit" + }, + "dependencies": { + "@agentic/core": "workspace:*", + "duck-duck-scrape": "^2.2.7", + "string-strip-html": "^13.4.12" + }, + "peerDependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@agentic/tsconfig": "workspace:*" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/legacy/packages/duck-duck-go/readme.md b/legacy/packages/duck-duck-go/readme.md new file mode 100644 index 00000000..38781f32 --- /dev/null +++ b/legacy/packages/duck-duck-go/readme.md @@ -0,0 +1,24 @@ +

+ + Agentic + +

+ +

+ AI agent stdlib that works with any LLM and TypeScript AI SDK. +

+ +

+ Build Status + NPM + MIT License + Prettier Code Formatting +

+ +# Agentic + +**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.** + +## License + +MIT © [Travis Fischer](https://x.com/transitive_bs) diff --git a/legacy/packages/duck-duck-go/src/duck-duck-go-client.ts b/legacy/packages/duck-duck-go/src/duck-duck-go-client.ts new file mode 100644 index 00000000..0b794889 --- /dev/null +++ b/legacy/packages/duck-duck-go/src/duck-duck-go-client.ts @@ -0,0 +1,78 @@ +import { aiFunction, AIFunctionsProvider } from '@agentic/core' +import { SafeSearchType, search, type SearchOptions } from 'duck-duck-scrape' +import { z } from 'zod' + +import { paginate } from './paginate' + +export namespace duckduckgo { + export interface DuckDuckGoSearchToolOptions { + search?: SearchOptions + maxResults: number + } + + export interface DuckDuckGoSearchToolRunOptions { + search?: SearchOptions + } +} +/** + * DuckDuckGo search client. + * + * @see https://duckduckgo.com + */ +export class DuckDuckGoClient extends AIFunctionsProvider { + /** + * Searches the web using DuckDuckGo for a given query. + */ + @aiFunction({ + name: 'duck_duck_go_search', + description: 'Searches the web using DuckDuckGo for a given query.', + inputSchema: z.object({ + query: z.string({ description: 'Search query' }).min(1).max(128), + maxResults: z.number().min(1).max(100).optional() + }) + }) + async search( + queryOrOptions: + | string + | { query: string; maxResults?: number; search?: SearchOptions } + ) { + const options = + typeof queryOrOptions === 'string' + ? { query: queryOrOptions } + : queryOrOptions + + const results = await paginate({ + size: options.maxResults ?? 10, + handler: async ({ cursor = 0 }) => { + const { results: data, noResults: done } = await search( + options.query, + { + safeSearch: SafeSearchType.MODERATE, + ...options.search, + offset: cursor + }, + { + uri_modifier: (rawUrl: string) => { + const url = new URL(rawUrl) + url.searchParams.delete('ss_mkt') + return url.toString() + } + } + ) + + return { + data, + nextCursor: done ? undefined : cursor + data.length + } + } + }) + + const { stripHtml } = await import('string-strip-html') + + return results.map((result) => ({ + url: result.url, + title: stripHtml(result.title).result, + description: stripHtml(result.description).result + })) + } +} diff --git a/legacy/packages/duck-duck-go/src/index.ts b/legacy/packages/duck-duck-go/src/index.ts new file mode 100644 index 00000000..4340e479 --- /dev/null +++ b/legacy/packages/duck-duck-go/src/index.ts @@ -0,0 +1 @@ +export * from './duck-duck-go-client' diff --git a/legacy/packages/duck-duck-go/src/paginate.ts b/legacy/packages/duck-duck-go/src/paginate.ts new file mode 100644 index 00000000..51791e95 --- /dev/null +++ b/legacy/packages/duck-duck-go/src/paginate.ts @@ -0,0 +1,34 @@ +export interface PaginateInput { + size: number + handler: (data: { + cursor?: C + limit: number + }) => Promise<{ data: T[]; nextCursor?: C }> +} + +export async function paginate( + input: PaginateInput +): Promise { + const acc: T[] = [] + let cursor: C | undefined + + while (acc.length < input.size) { + const { data, nextCursor } = await input.handler({ + cursor, + limit: input.size - acc.length + }) + acc.push(...data) + + if (nextCursor === undefined || data.length === 0) { + break + } + + cursor = nextCursor + } + + if (acc.length > input.size) { + acc.length = input.size + } + + return acc +} diff --git a/legacy/packages/duck-duck-go/tsconfig.json b/legacy/packages/duck-duck-go/tsconfig.json new file mode 100644 index 00000000..6c8d720c --- /dev/null +++ b/legacy/packages/duck-duck-go/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@agentic/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/legacy/packages/mcp/package.json b/legacy/packages/mcp/package.json new file mode 100644 index 00000000..3832311a --- /dev/null +++ b/legacy/packages/mcp/package.json @@ -0,0 +1,46 @@ +{ + "name": "@agentic/mcp", + "version": "0.1.0", + "description": "Agentic SDK wrapping an MCP client.", + "author": "Travis Fischer ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/transitive-bullshit/agentic.git" + }, + "type": "module", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "del dist", + "test": "run-s test:*", + "test:lint": "eslint .", + "test:typecheck": "tsc --noEmit" + }, + "dependencies": { + "@agentic/core": "workspace:*", + "@modelcontextprotocol/sdk": "catalog:" + }, + "peerDependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@agentic/tsconfig": "workspace:*" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/legacy/packages/mcp/readme.md b/legacy/packages/mcp/readme.md new file mode 100644 index 00000000..38781f32 --- /dev/null +++ b/legacy/packages/mcp/readme.md @@ -0,0 +1,24 @@ +

+ + Agentic + +

+ +

+ AI agent stdlib that works with any LLM and TypeScript AI SDK. +

+ +

+ Build Status + NPM + MIT License + Prettier Code Formatting +

+ +# Agentic + +**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.** + +## License + +MIT © [Travis Fischer](https://x.com/transitive_bs) diff --git a/legacy/packages/mcp/src/index.ts b/legacy/packages/mcp/src/index.ts new file mode 100644 index 00000000..dcba8e48 --- /dev/null +++ b/legacy/packages/mcp/src/index.ts @@ -0,0 +1,2 @@ +export * from './mcp-tools' +export type * from './types' diff --git a/legacy/packages/mcp/src/mcp-tools.ts b/legacy/packages/mcp/src/mcp-tools.ts new file mode 100644 index 00000000..187fcb5b --- /dev/null +++ b/legacy/packages/mcp/src/mcp-tools.ts @@ -0,0 +1,216 @@ +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import type { + CallToolResult, + ListToolsResult +} from '@modelcontextprotocol/sdk/types.js' +import { + AIFunctionSet, + AIFunctionsProvider, + assert, + createAIFunction, + createJsonSchema +} from '@agentic/core' +import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js' +import { type z } from 'zod' + +import type { McpClientOptions, McpToolsFilter } from './types' +import { paginate } from './paginate' + +/** + * Agentic tools provider wrapping an MCP client. + * + * You likely want to use `createMcpTools` to create an instance of `McpTools` + * which enables exposing MCP server tools to the agentic ecosystem. + * + * @see https://modelcontextprotocol.io + */ +export class McpTools extends AIFunctionsProvider { + readonly name: string + readonly client: McpClient + readonly rawToolResponses: boolean + + protected _toolsMap: Map | undefined + protected readonly _toolsFilter: McpToolsFilter | undefined + + protected constructor({ + name, + client, + toolsFilter, + rawToolResponses = false + }: { + client: McpClient + } & McpClientOptions) { + super() + + this.name = name + this.client = client + this.rawToolResponses = rawToolResponses + + this._toolsFilter = toolsFilter + } + + override get functions(): AIFunctionSet { + assert(this._functions) + return this._functions + } + + /** + * Initialize the MCPTools instance by fetching all available tools from the MCP client. + * This method must be called before using this class' tools. + * It is called automatically when using `MCPTools.from()`. + */ + protected async _init() { + const capabilties = this.client.getServerCapabilities() + const initPromises: Promise[] = [] + + if (capabilties?.tools) { + initPromises.push(this._initTools()) + } + + // TODO: handle prompts, resources, etc. + await Promise.all(initPromises) + } + + protected async _initTools() { + const tools = await paginate({ + size: Infinity, + handler: async ({ cursor }: { cursor?: string }) => { + const { tools, nextCursor } = await this.client.listTools({ cursor }) + return { data: tools, nextCursor } as const + } + }) + + const enabledTools = this._toolsFilter + ? tools.filter((tool) => this._toolsFilter!(tool.name)) + : tools + + this._toolsMap = new Map(enabledTools.map((tool) => [tool.name, tool])) + this._updateFunctions() + } + + protected _updateFunctions() { + assert(this._toolsMap) + + this._functions = new AIFunctionSet( + Array.from(this._toolsMap.entries()).map(([_name, tool]) => { + return createAIFunction( + { + name: `${this.name}_${tool.name}`, + description: tool.description, + inputSchema: createJsonSchema(tool.inputSchema), + strict: true + }, + async (args) => { + const result = await this.client.callTool({ + name: tool.name, + arguments: args + }) + + if (this.rawToolResponses) { + return result + } + + return processToolCallResult(result as CallToolResult) + } + ) + }) + ) + } + + async callTool(name: string, args: z.infer>) { + const tool = + this._toolsMap?.get(name) ?? this._toolsMap?.get(`${this.name}_${name}`) + assert(tool, `Tool ${name} not found`) + + const result = await this.client.callTool({ name, arguments: args }) + return result + } + + /** + * Creates a new McpTools instance from an existing, fully initialized + * MCP client. + * + * You probably want to use `createMcpTool` instead, which makes initializing + * the MCP client and connecting to its transport easier. + * + * All tools within the `McpTools` instance will be namespaced under the given + * `name`. + */ + static async fromMcpClient(params: { client: McpClient } & McpClientOptions) { + const mcpTools = new McpTools(params) + await mcpTools._init() + return mcpTools + } +} + +/** + * Creates a new McpTools instance by connecting to an MCP server. You must + * provide either an existing `transport`, an existing `serverUrl`, or a + * `serverProcess` to spawn. + * + * All tools within the `McpTools` instance will be namespaced under the given + * `name`. + */ +export async function createMcpTools( + params: McpClientOptions +): Promise { + const transport = await createMcpTransport(params) + const client = new McpClient( + { name: params.name, version: params.version || '1.0.0' }, + { capabilities: {} } + ) + await client.connect(transport) + + return McpTools.fromMcpClient({ client, ...params }) +} + +/** + * Creates a new MCP transport from either an existing `transport`, an existing + * `serverUrl`, or a `serverProcess` to spawn. + */ +export async function createMcpTransport( + params: McpClientOptions +): Promise { + if (params.transport) return params.transport + + if (params.serverUrl) { + const { SSEClientTransport } = await import( + '@modelcontextprotocol/sdk/client/sse.js' + ) + return new SSEClientTransport(new URL(params.serverUrl)) + } + + if (params.serverProcess) { + const { StdioClientTransport } = await import( + '@modelcontextprotocol/sdk/client/stdio.js' + ) + return new StdioClientTransport(params.serverProcess) + } + + throw new Error( + 'Unable to create a server connection with supplied options. Must provide transport, stdio, or sseUrl.' + ) +} + +function toText(c: CallToolResult['content']) { + return c.map((p) => p.text || '').join('') +} + +function processToolCallResult(result: CallToolResult) { + if (result.isError) return { error: toText(result.content) } + + if (result.content.every((c) => !!c.text)) { + const text = toText(result.content) + if (text.trim().startsWith('{') || text.trim().startsWith('[')) { + try { + return JSON.parse(text) + } catch { + return text + } + } + return text + } + + if (result.content.length === 1) return result.content[0] + return result +} diff --git a/legacy/packages/mcp/src/paginate.ts b/legacy/packages/mcp/src/paginate.ts new file mode 100644 index 00000000..51791e95 --- /dev/null +++ b/legacy/packages/mcp/src/paginate.ts @@ -0,0 +1,34 @@ +export interface PaginateInput { + size: number + handler: (data: { + cursor?: C + limit: number + }) => Promise<{ data: T[]; nextCursor?: C }> +} + +export async function paginate( + input: PaginateInput +): Promise { + const acc: T[] = [] + let cursor: C | undefined + + while (acc.length < input.size) { + const { data, nextCursor } = await input.handler({ + cursor, + limit: input.size - acc.length + }) + acc.push(...data) + + if (nextCursor === undefined || data.length === 0) { + break + } + + cursor = nextCursor + } + + if (acc.length > input.size) { + acc.length = input.size + } + + return acc +} diff --git a/legacy/packages/mcp/src/types.ts b/legacy/packages/mcp/src/types.ts new file mode 100644 index 00000000..2ae26949 --- /dev/null +++ b/legacy/packages/mcp/src/types.ts @@ -0,0 +1,53 @@ +import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' + +export type McpToolsFilter = (toolName: string) => boolean + +export interface McpClientOptions { + /** + * Provide a name for this client which will be its namespace for all tools and prompts. + */ + name: string + + /** + * Provide a version number for this client (defaults to 1.0.0). + */ + version?: string + + /** + * If you already have an MCP transport you'd like to use, pass it here to connect to the server. + */ + transport?: Transport + + /** + * Start a local server process using the stdio MCP transport. + */ + serverProcess?: StdioServerParameters + + /** + * Connect to a remote server process using the SSE MCP transport. + */ + serverUrl?: string + + /** + * Return tool responses in raw MCP form instead of processing them for Genkit compatibility. + */ + rawToolResponses?: boolean + + /** + * An optional filter function to determine which tools should be enabled. + * + * By default, all tools available on the MCP server will be enabled, but you + * can use this to filter a subset of those tools. + */ + toolsFilter?: McpToolsFilter +} + +// TODO +// export interface McpServerOptions { +// /** The name you want to give your server for MCP inspection. */ +// name: string +// +// /** The version you want the server to advertise to clients. Defaults to 1.0.0. */ +// version?: string +// } diff --git a/legacy/packages/mcp/tsconfig.json b/legacy/packages/mcp/tsconfig.json new file mode 100644 index 00000000..6c8d720c --- /dev/null +++ b/legacy/packages/mcp/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@agentic/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +}