feat: add MCP, arxiv, and duck-duck-go tools

pull/700/head
Travis Fischer 2025-03-24 00:46:55 +08:00
rodzic 2db62b0ca2
commit 1a7d3ab732
23 zmienionych plików z 1126 dodań i 0 usunięć

Wyświetl plik

@ -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
<CodeGroup>
```bash npm
npm install @agentic/arxiv
```
```bash yarn
yarn add @agentic/arxiv
```
```bash pnpm
pnpm add @agentic/arxiv
```
</CodeGroup>
## 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
})
```

Wyświetl plik

@ -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
<CodeGroup>
```bash npm
npm install @agentic/duck-duck-go
```
```bash yarn
yarn add @agentic/duck-duck-go
```
```bash pnpm
pnpm add @agentic/duck-duck-go
```
</CodeGroup>
## 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'
})
```

85
docs/tools/mcp.mdx 100644
Wyświetl plik

@ -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
<CodeGroup>
```bash npm
npm install @agentic/mcp
```
```bash yarn
yarn add @agentic/mcp
```
```bash pnpm
pnpm add @agentic/mcp
```
</CodeGroup>
## 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`.

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -0,0 +1,47 @@
{
"name": "@agentic/arxiv",
"version": "0.1.0",
"description": "Agentic SDK for Arxiv.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"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"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# 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)

Wyświetl plik

@ -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 extends NonNullable<unknown>> = T[keyof T]
export const FilterTypeMapping: Record<ValueOf<typeof FilterType>, 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<typeof SearchParamsSchema>
}
/**
* 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<string, any>[] = 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)
})
)
}
}
}

Wyświetl plik

@ -0,0 +1 @@
export * from './arxiv-client'

Wyświetl plik

@ -0,0 +1,30 @@
export function hasProp<T>(
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<T>(arr: T) {
const result = Array.isArray(arr) ? arr : [arr]
return result as T extends unknown[] ? T : [T]
}

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@agentic/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

Wyświetl plik

@ -0,0 +1,48 @@
{
"name": "@agentic/duck-duck-go",
"version": "7.5.3",
"description": "Agentic SDK for DuckDuckGo.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"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"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# 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)

Wyświetl plik

@ -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
}))
}
}

Wyświetl plik

@ -0,0 +1 @@
export * from './duck-duck-go-client'

Wyświetl plik

@ -0,0 +1,34 @@
export interface PaginateInput<T, C> {
size: number
handler: (data: {
cursor?: C
limit: number
}) => Promise<{ data: T[]; nextCursor?: C }>
}
export async function paginate<T, C = number>(
input: PaginateInput<T, C>
): Promise<T[]> {
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
}

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@agentic/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

Wyświetl plik

@ -0,0 +1,46 @@
{
"name": "@agentic/mcp",
"version": "0.1.0",
"description": "Agentic SDK wrapping an MCP client.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"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"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# 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)

Wyświetl plik

@ -0,0 +1,2 @@
export * from './mcp-tools'
export type * from './types'

Wyświetl plik

@ -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<string, ListToolsResult['tools'][number]> | 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<any>[] = []
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<z.ZodObject<any>>) {
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<McpTools> {
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<Transport> {
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
}

Wyświetl plik

@ -0,0 +1,34 @@
export interface PaginateInput<T, C> {
size: number
handler: (data: {
cursor?: C
limit: number
}) => Promise<{ data: T[]; nextCursor?: C }>
}
export async function paginate<T, C = number>(
input: PaginateInput<T, C>
): Promise<T[]> {
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
}

Wyświetl plik

@ -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
// }

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@agentic/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}