kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add MCP, arxiv, and duck-duck-go tools
rodzic
2db62b0ca2
commit
1a7d3ab732
|
@ -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
|
||||
})
|
||||
```
|
|
@ -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'
|
||||
})
|
||||
```
|
|
@ -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`.
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './arxiv-client'
|
|
@ -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]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@agentic/tsconfig/base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './duck-duck-go-client'
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@agentic/tsconfig/base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
export * from './mcp-tools'
|
||||
export type * from './types'
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
// }
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@agentic/tsconfig/base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Ładowanie…
Reference in New Issue