diff --git a/README.md b/README.md index 9a70533..e9ddd7e 100644 --- a/README.md +++ b/README.md @@ -281,8 +281,7 @@ npm install @stackone/ai @anthropic-ai/claude-agent-sdk zod # or: yarn/pnpm/bun ``` ```typescript -import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"; -import { z } from "zod"; +import { query } from "@anthropic-ai/claude-agent-sdk"; import { StackOneToolSet } from "@stackone/ai"; const toolset = new StackOneToolSet({ @@ -290,26 +289,9 @@ const toolset = new StackOneToolSet({ accountId: "your-account-id", }); +// Fetch tools and convert to Claude Agent SDK format const tools = await toolset.fetchTools(); -const employeeTool = tools.getTool("bamboohr_get_employee"); - -// Create a Claude Agent SDK tool from the StackOne tool -const getEmployeeTool = tool( - employeeTool.name, - employeeTool.description, - { id: z.string().describe("The employee ID") }, - async (args) => { - const result = await employeeTool.execute(args); - return { content: [{ type: "text", text: JSON.stringify(result) }] }; - } -); - -// Create an MCP server with the StackOne tool -const mcpServer = createSdkMcpServer({ - name: "stackone-tools", - version: "1.0.0", - tools: [getEmployeeTool], -}); +const mcpServer = await tools.toClaudeAgentSdk(); // Use with Claude Agent SDK query const result = query({ diff --git a/examples/claude-agent-sdk-integration.ts b/examples/claude-agent-sdk-integration.ts index 5008f78..5e8fd22 100644 --- a/examples/claude-agent-sdk-integration.ts +++ b/examples/claude-agent-sdk-integration.ts @@ -3,12 +3,14 @@ * * Claude Agent SDK allows you to create autonomous agents with custom tools * via MCP (Model Context Protocol) servers. + * + * The `toClaudeAgentSdk()` method automatically converts StackOne tools + * to Claude Agent SDK format, handling the MCP server creation internally. */ import assert from 'node:assert'; import process from 'node:process'; -import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; -import { z } from 'zod'; +import { query } from '@anthropic-ai/claude-agent-sdk'; import { StackOneToolSet } from '@stackone/ai'; const apiKey = process.env.STACKONE_API_KEY; @@ -18,7 +20,7 @@ if (!apiKey) { } // Replace with your actual account ID from StackOne dashboard -const accountId = 'your-bamboohr-account-id'; +const accountId = 'your-hris-account-id'; const claudeAgentSdkIntegration = async (): Promise => { // Initialize StackOne @@ -27,42 +29,19 @@ const claudeAgentSdkIntegration = async (): Promise => { baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', }); - // Fetch tools from StackOne + // Fetch tools from StackOne and convert to Claude Agent SDK format const tools = await toolset.fetchTools(); + const mcpServer = await tools.toClaudeAgentSdk(); - // Get a specific tool - const employeeTool = tools.getTool('bamboohr_get_employee'); - assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool'); - - // Create a Claude Agent SDK tool from the StackOne tool - const getEmployeeTool = tool( - employeeTool.name, - employeeTool.description, - { - id: z.string().describe('The employee ID'), - }, - async (args) => { - const result = await employeeTool.execute(args); - return { - content: [{ type: 'text', text: JSON.stringify(result) }], - }; - }, - ); - - // Create an MCP server with the StackOne tool - const mcpServer = createSdkMcpServer({ - name: 'stackone-tools', - version: '1.0.0', - tools: [getEmployeeTool], - }); - - // Use the Claude Agent SDK query with the custom MCP server + // Use the Claude Agent SDK query with the StackOne MCP server + // Type assertion is needed because our interface is compatible but not identical const result = query({ prompt: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', options: { model: 'claude-sonnet-4-5-20250929', mcpServers: { - 'stackone-tools': mcpServer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Compatible MCP server type + 'stackone-tools': mcpServer as any, }, // Disable built-in tools, only use our custom tools tools: [], @@ -75,14 +54,14 @@ const claudeAgentSdkIntegration = async (): Promise => { for await (const message of result) { if (message.type === 'assistant') { for (const block of message.message.content) { - if (block.type === 'tool_use' && block.name === 'bamboohr_get_employee') { + if (block.type === 'tool_use' && block.name === 'hris_get_employee') { hasToolCall = true; } } } } - assert(hasToolCall, 'Expected at least one tool call to bamboohr_get_employee'); + assert(hasToolCall, 'Expected at least one tool call to hris_get_employee'); }; await claudeAgentSdkIntegration(); diff --git a/package.json b/package.json index 1bcb316..6882cd0 100644 --- a/package.json +++ b/package.json @@ -81,12 +81,16 @@ "zod": "catalog:dev" }, "peerDependencies": { + "@anthropic-ai/claude-agent-sdk": "catalog:peer", "@anthropic-ai/sdk": "catalog:peer", "ai": "catalog:peer", "openai": "catalog:peer", "zod": "catalog:peer" }, "peerDependenciesMeta": { + "@anthropic-ai/claude-agent-sdk": { + "optional": true + }, "@anthropic-ai/sdk": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bafca89..73d0d0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,8 +70,8 @@ catalogs: specifier: ^4.0.15 version: 4.0.15 zod: - specifier: ^4.1.13 - version: 4.1.13 + specifier: ^4.3.0 + version: 4.3.5 examples: '@anthropic-ai/claude-agent-sdk': specifier: ^0.1.67 @@ -83,6 +83,9 @@ catalogs: specifier: ^0.2.0 version: 0.2.0 peer: + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.1.67 + version: 0.1.67 '@anthropic-ai/sdk': specifier: ^0.52.0 version: 0.52.0 @@ -107,12 +110,15 @@ importers: .: dependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: catalog:peer + version: 0.1.67(zod@4.3.5) '@anthropic-ai/sdk': specifier: catalog:peer version: 0.52.0 '@modelcontextprotocol/sdk': specifier: catalog:prod - version: 1.24.3(zod@4.1.13) + version: 1.24.3(zod@4.3.5) '@orama/orama': specifier: catalog:prod version: 3.1.16 @@ -125,7 +131,7 @@ importers: version: 0.2.4(vitest@4.0.15(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(jiti@2.6.1)(msw@2.12.3(@types/node@22.19.1)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2)) '@hono/mcp': specifier: catalog:dev - version: 0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(hono@4.10.7) + version: 0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.3.5))(hono@4.10.7) '@types/node': specifier: catalog:dev version: 22.19.1 @@ -137,7 +143,7 @@ importers: version: 4.0.15(vitest@4.0.15(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(jiti@2.6.1)(msw@2.12.3(@types/node@22.19.1)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2)) ai: specifier: catalog:dev - version: 6.0.7(zod@4.1.13) + version: 6.0.7(zod@4.3.5) hono: specifier: catalog:dev version: 4.10.7 @@ -155,7 +161,7 @@ importers: version: runtime:24.12.0 openai: specifier: catalog:peer - version: 6.9.1(zod@4.1.13) + version: 6.9.1(zod@4.3.5) oxfmt: specifier: catalog:dev version: 0.18.0 @@ -185,16 +191,16 @@ importers: version: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(jiti@2.6.1)(msw@2.12.3(@types/node@22.19.1)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) zod: specifier: catalog:dev - version: 4.1.13 + version: 4.3.5 examples: dependencies: '@ai-sdk/openai': specifier: catalog:dev - version: 3.0.2(zod@4.1.13) + version: 3.0.2(zod@4.3.5) '@anthropic-ai/claude-agent-sdk': specifier: catalog:examples - version: 0.1.67(zod@4.1.13) + version: 0.1.67(zod@4.3.5) '@anthropic-ai/sdk': specifier: catalog:peer version: 0.52.0 @@ -209,16 +215,16 @@ importers: version: 0.2.0 '@tanstack/ai-openai': specifier: catalog:examples - version: 0.2.0(@tanstack/ai@0.2.0)(zod@4.1.13) + version: 0.2.0(@tanstack/ai@0.2.0)(zod@4.3.5) ai: specifier: catalog:peer - version: 6.0.7(zod@4.1.13) + version: 6.0.7(zod@4.3.5) openai: specifier: catalog:peer - version: 6.9.1(zod@4.1.13) + version: 6.9.1(zod@4.3.5) zod: specifier: catalog:dev - version: 4.1.13 + version: 4.3.5 devDependencies: '@types/node': specifier: catalog:dev @@ -2560,38 +2566,38 @@ packages: peerDependencies: zod: ^3.25 || ^4 - zod@4.1.13: - resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} snapshots: - '@ai-sdk/gateway@3.0.6(zod@4.1.13)': + '@ai-sdk/gateway@3.0.6(zod@4.3.5)': dependencies: '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.13) + '@ai-sdk/provider-utils': 4.0.2(zod@4.3.5) '@vercel/oidc': 3.0.5 - zod: 4.1.13 + zod: 4.3.5 - '@ai-sdk/openai@3.0.2(zod@4.1.13)': + '@ai-sdk/openai@3.0.2(zod@4.3.5)': dependencies: '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.13) - zod: 4.1.13 + '@ai-sdk/provider-utils': 4.0.2(zod@4.3.5) + zod: 4.3.5 - '@ai-sdk/provider-utils@4.0.2(zod@4.1.13)': + '@ai-sdk/provider-utils@4.0.2(zod@4.3.5)': dependencies: '@ai-sdk/provider': 3.0.1 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.1.13 + zod: 4.3.5 '@ai-sdk/provider@3.0.1': dependencies: json-schema: 0.4.0 - '@anthropic-ai/claude-agent-sdk@0.1.67(zod@4.1.13)': + '@anthropic-ai/claude-agent-sdk@0.1.67(zod@4.3.5)': dependencies: - zod: 4.1.13 + zod: 4.3.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -2815,9 +2821,9 @@ snapshots: fast-check: 4.5.2 vitest: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(jiti@2.6.1)(msw@2.12.3(@types/node@22.19.1)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - '@hono/mcp@0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(hono@4.10.7)': + '@hono/mcp@0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.3.5))(hono@4.10.7)': dependencies: - '@modelcontextprotocol/sdk': 1.24.3(zod@4.1.13) + '@modelcontextprotocol/sdk': 1.24.3(zod@4.3.5) hono: 4.10.7 '@img/sharp-darwin-arm64@0.33.5': @@ -2926,7 +2932,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.24.3(zod@4.1.13)': + '@modelcontextprotocol/sdk@1.24.3(zod@4.3.5)': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -2940,8 +2946,8 @@ snapshots: jose: 6.1.3 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.1.13 - zod-to-json-schema: 3.25.0(zod@4.1.13) + zod: 4.3.5 + zod-to-json-schema: 3.25.0(zod@4.3.5) transitivePeerDependencies: - supports-color @@ -3233,11 +3239,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tanstack/ai-openai@0.2.0(@tanstack/ai@0.2.0)(zod@4.1.13)': + '@tanstack/ai-openai@0.2.0(@tanstack/ai@0.2.0)(zod@4.3.5)': dependencies: '@tanstack/ai': 0.2.0 - openai: 6.9.1(zod@4.1.13) - zod: 4.1.13 + openai: 6.9.1(zod@4.3.5) + zod: 4.3.5 transitivePeerDependencies: - ws @@ -3365,13 +3371,13 @@ snapshots: acorn@8.15.0: {} - ai@6.0.7(zod@4.1.13): + ai@6.0.7(zod@4.3.5): dependencies: - '@ai-sdk/gateway': 3.0.6(zod@4.1.13) + '@ai-sdk/gateway': 3.0.6(zod@4.3.5) '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.1.13) + '@ai-sdk/provider-utils': 4.0.2(zod@4.3.5) '@opentelemetry/api': 1.9.0 - zod: 4.1.13 + zod: 4.3.5 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: @@ -3820,7 +3826,7 @@ snapshots: smol-toml: 1.5.2 strip-json-comments: 5.0.3 typescript: 5.9.3 - zod: 4.1.13 + zod: 4.3.5 lefthook-darwin-arm64@2.0.8: optional: true @@ -3951,9 +3957,9 @@ snapshots: dependencies: wrappy: 1.0.2 - openai@6.9.1(zod@4.1.13): + openai@6.9.1(zod@4.3.5): optionalDependencies: - zod: 4.1.13 + zod: 4.3.5 outvariant@1.4.3: {} @@ -4480,8 +4486,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} - zod-to-json-schema@3.25.0(zod@4.1.13): + zod-to-json-schema@3.25.0(zod@4.3.5): dependencies: - zod: 4.1.13 + zod: 4.3.5 - zod@4.1.13: {} + zod@4.3.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1a6c0b3..d1a77c6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,12 +27,13 @@ catalogs: typescript: ^5.8.3 unplugin-unused: ^0.5.4 vitest: ^4.0.15 - zod: ^4.1.13 + zod: ^4.3.0 examples: '@anthropic-ai/claude-agent-sdk': ^0.1.67 '@tanstack/ai': ^0.2.0 '@tanstack/ai-openai': ^0.2.0 peer: + '@anthropic-ai/claude-agent-sdk': ^0.1.67 '@anthropic-ai/sdk': ^0.52.0 ai: '>=5.0.108 <7.0.0' openai: ^6.2.0 diff --git a/src/tool.test.ts b/src/tool.test.ts index 876f256..0291844 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -171,6 +171,25 @@ describe('StackOneTool', () => { expect(schema.properties?.id.type).toBe('string'); }); + it('should convert to Claude Agent SDK tool format', async () => { + const tool = createMockTool(); + + const claudeTool = await tool.toClaudeAgentSdkTool(); + + expect(claudeTool).toBeDefined(); + expect(claudeTool.name).toBe('test_tool'); + expect(claudeTool.description).toBe('Test tool'); + expect(claudeTool.inputSchema).toBeDefined(); + expect(typeof claudeTool.handler).toBe('function'); + + // Test the handler returns content in the expected format + const result = await claudeTool.handler({ id: 'test-123' }); + expect(result).toHaveProperty('content'); + expect(Array.isArray(result.content)).toBe(true); + expect(result.content[0]).toHaveProperty('type', 'text'); + expect(result.content[0]).toHaveProperty('text'); + }); + it('should include execution metadata by default in AI SDK conversion', async () => { const tool = createMockTool(); @@ -693,6 +712,58 @@ describe('Tools', () => { expect(typeof aiSdkTools.another_tool.execute).toBe('function'); }); + it('should convert all tools to Claude Agent SDK MCP server', async () => { + const tool1 = createMockTool(); + const tool2 = new StackOneTool( + 'another_tool', + 'Another tool', + { + type: 'object', + properties: { name: { type: 'string' } }, + }, + { + kind: 'http', + method: 'POST', + url: 'https://api.example.com/test', + bodyType: 'json', + params: [ + { + name: 'name', + location: ParameterLocation.BODY, + type: 'string', + }, + ], + }, + { + authorization: 'Bearer test_key', + }, + ); + + const tools = new Tools([tool1, tool2]); + + const mcpServer = await tools.toClaudeAgentSdk(); + + expect(mcpServer).toBeDefined(); + expect(mcpServer.type).toBe('sdk'); + expect(mcpServer.name).toBe('stackone-tools'); + expect(mcpServer.instance).toBeDefined(); + }); + + it('should convert all tools to Claude Agent SDK MCP server with custom options', async () => { + const tool1 = createMockTool(); + const tools = new Tools([tool1]); + + const mcpServer = await tools.toClaudeAgentSdk({ + serverName: 'my-custom-server', + serverVersion: '2.0.0', + }); + + expect(mcpServer).toBeDefined(); + expect(mcpServer.type).toBe('sdk'); + expect(mcpServer.name).toBe('my-custom-server'); + expect(mcpServer.instance).toBeDefined(); + }); + it('should be iterable', () => { const tool1 = createMockTool(); const tool2 = new StackOneTool( diff --git a/src/tool.ts b/src/tool.ts index f574562..bd97259 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,5 +1,6 @@ -import type { JSONSchema7 as AISDKJSONSchema } from 'ai'; +import { type JSONSchema7 as AISDKJSONSchema, jsonSchema } from 'ai'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; +import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; import type { FunctionTool as OpenAIResponsesFunctionTool } from 'openai/resources/responses/responses'; @@ -10,6 +11,7 @@ import { RequestBuilder } from './requestBuilder'; import type { AISDKToolDefinition, AISDKToolResult, + ClaudeAgentSdkOptions, ExecuteConfig, ExecuteOptions, HttpExecuteConfig, @@ -225,6 +227,36 @@ export class BaseTool { }; } + /** + * Convert the tool to Claude Agent SDK format. + * Returns a tool definition compatible with the Claude Agent SDK's tool() function. + * + * @see https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk + */ + async toClaudeAgentSdkTool(): Promise<{ + name: string; + description: string; + inputSchema: Record; + handler: ( + args: Record, + ) => Promise<{ content: Array<{ type: 'text'; text: string }> }>; + }> { + const inputSchema = jsonSchema(this.toJsonSchema()); + const execute = this.execute.bind(this); + + return { + name: this.name, + description: this.description, + inputSchema, + handler: async (args: Record) => { + const result = await execute(args as JsonObject); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result) }], + }; + }, + }; + } + /** * Convert the tool to AI SDK format */ @@ -395,6 +427,64 @@ export class Tools implements Iterable { return result; } + /** + * Convert all tools to Claude Agent SDK format. + * Returns an MCP server configuration that can be passed to the + * Claude Agent SDK query() function's mcpServers option. + * + * @example + * ```typescript + * const tools = await toolset.fetchTools(); + * const mcpServer = await tools.toClaudeAgentSdk(); + * + * const result = query({ + * prompt: 'Get employee info', + * options: { + * model: 'claude-sonnet-4-5-20250929', + * mcpServers: { + * 'stackone-tools': mcpServer, + * }, + * }, + * }); + * ``` + * + * @see https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk + */ + async toClaudeAgentSdk( + options: ClaudeAgentSdkOptions = {}, + ): Promise { + const { serverName = 'stackone-tools', serverVersion = '1.0.0' } = options; + + // Import the Claude Agent SDK dynamically + const claudeAgentSdk = await tryImport( + '@anthropic-ai/claude-agent-sdk', + `npm install @anthropic-ai/claude-agent-sdk (requires ${peerDependencies['@anthropic-ai/claude-agent-sdk']})`, + ); + + // Convert all tools to Claude Agent SDK format + // We use type assertions here because the Zod types from our dynamic import + // don't perfectly match the Claude Agent SDK's expected types at compile time + const sdkTools = await Promise.all( + this.tools.map(async (baseTool) => { + const toolDef = await baseTool.toClaudeAgentSdkTool(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic Zod schema types + return claudeAgentSdk.tool( + toolDef.name, + toolDef.description, + toolDef.inputSchema as any, + toolDef.handler as any, + ); + }), + ); + + // Create and return the MCP server + return claudeAgentSdk.createSdkMcpServer({ + name: serverName, + version: serverVersion, + tools: sdkTools, + }); + } + /** * Filter tools by a predicate function */ diff --git a/src/types.ts b/src/types.ts index c8e10e3..0190381 100644 --- a/src/types.ts +++ b/src/types.ts @@ -201,3 +201,17 @@ export type AISDKToolDefinition = Tool & { export type AISDKToolResult = ToolSet & { [K in T]: AISDKToolDefinition; }; + +/** + * Options for toClaudeAgentSdk() method + */ +export interface ClaudeAgentSdkOptions { + /** + * Name of the MCP server. Defaults to 'stackone-tools'. + */ + serverName?: string; + /** + * Version of the MCP server. Defaults to '1.0.0'. + */ + serverVersion?: string; +}