From cf5dbe9f8877d578fa61f77d5f90db87b7a6b0d3 Mon Sep 17 00:00:00 2001 From: aryan Date: Fri, 14 Feb 2025 14:45:53 -0700 Subject: [PATCH 1/3] feat: mcp --- package.json | 2 + pnpm-lock.yaml | 42 +++++++++++ src/mcp/index.ts | 134 ++++++++++++++++++++++++++++++++++++ src/utils/zodToMCPSchema.ts | 48 +++++++++++++ test/mcp.ts | 21 ++++++ 5 files changed, 247 insertions(+) create mode 100644 src/mcp/index.ts create mode 100644 src/utils/zodToMCPSchema.ts create mode 100644 test/mcp.ts diff --git a/package.json b/package.json index 040372bf..b458d638 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "docs": "typedoc src --out docs", "test": "tsx test/index.ts", "test:vercel-ai": "tsx test/agent_sdks/vercel_ai.ts", + "test:mcp": "tsx test/mcp.ts", "generate": "tsx src/utils/keypair.ts", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", @@ -58,6 +59,7 @@ "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", "@meteora-ag/alpha-vault": "^1.1.7", "@meteora-ag/dlmm": "^1.3.0", + "@modelcontextprotocol/sdk": "^1.5.0", "@onsol/tldparser": "^0.6.7", "@openzeppelin/contracts": "^5.2.0", "@orca-so/common-sdk": "0.6.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1520abe..bfe78d3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: '@meteora-ag/dlmm': specifier: ^1.3.0 version: 1.3.8(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) + '@modelcontextprotocol/sdk': + specifier: ^1.5.0 + version: 1.5.0 '@onsol/tldparser': specifier: ^0.6.7 version: 0.6.7(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bn.js@5.2.1)(borsh@2.0.0)(buffer@6.0.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -1046,6 +1049,10 @@ packages: resolution: {integrity: sha512-FevXshZyeFD+CpYoYBrg95lRx8CyrhV5R31IteNzGlSRcQ6NWFRhTmgxtt+yMHFGj8+24qwfBUrBCNx2vT/G4A==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + '@modelcontextprotocol/sdk@1.5.0': + resolution: {integrity: sha512-IJ+5iVVs8FCumIHxWqpwgkwOzyhtHVKy45s6Ug7Dv0MfRpaYisH8QQ87rIWeWdOzlk8sfhitZ7HCyQZk7d6b8w==} + engines: {node: '>=18'} + '@msgpack/msgpack@2.8.0': resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} engines: {node: '>= 10'} @@ -2682,6 +2689,10 @@ packages: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} engines: {node: '>=12.0.0'} + eventsource@3.0.5: + resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} + engines: {node: '>=18.0.0'} + exec-limiter@3.2.13: resolution: {integrity: sha512-86Ri699bwiHZVBzTzNj8gspqAhCPchg70zPVWIh3qzUOA1pUMcb272Em3LPk8AE0mS95B9yMJhtqF8vFJAn0dA==} @@ -3046,6 +3057,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4048,6 +4063,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -6811,6 +6830,14 @@ snapshots: - typescript - utf-8-validate + '@modelcontextprotocol/sdk@1.5.0': + dependencies: + content-type: 1.0.5 + eventsource: 3.0.5 + raw-body: 3.0.0 + zod: 3.24.1 + zod-to-json-schema: 3.24.1(zod@3.24.1) + '@msgpack/msgpack@2.8.0': {} '@msgpack/msgpack@3.0.0-beta2': {} @@ -9479,6 +9506,10 @@ snapshots: eventsource@2.0.2: {} + eventsource@3.0.5: + dependencies: + eventsource-parser: 3.0.0 + exec-limiter@3.2.13: dependencies: limit-it: 3.2.10 @@ -10015,6 +10046,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -10983,6 +11018,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 00000000..d35726fc --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,134 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import type { Action } from "../types/action"; +import { SolanaAgentKit } from "../agent"; +import { zodToMCPShape } from "../utils/zodToMCPSchema"; + +/** + * Creates an MCP server from a set of actions + */ +export function createMcpServer( + actions: Record, + solanaAgentKit: SolanaAgentKit, + options: { + name: string; + version: string; + } +) { + // Create MCP server instance + const server = new McpServer({ + name: options.name, + version: options.version, + }); + + // Convert each action to an MCP tool + for (const [key, action] of Object.entries(actions)) { + server.tool( + action.name, + action.description, + zodToMCPShape(action.schema), + async (params) => { + try { + // Execute the action handler + const result = await action.handler(solanaAgentKit, params); + + // Format the result as MCP tool response + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2) + } + ] + }; + } catch (error) { + // Handle errors in MCP format + return { + isError: true, + content: [ + { + type: "text", + text: error instanceof Error ? error.message : "Unknown error occurred" + } + ] + }; + } + } + ); + + // Add examples as prompts if they exist + if (action.examples && action.examples.length > 0) { + server.prompt( + `${action.name}-examples`, + { + showIndex: z.string().optional().describe("Example index to show (number)") + }, + (args) => { + const showIndex = args.showIndex ? parseInt(args.showIndex) : undefined; + const examples = action.examples.flat(); + const selectedExamples = typeof showIndex === 'number' + ? [examples[showIndex]] + : examples; + + const exampleText = selectedExamples + .map((ex, idx) => ` +Example ${idx + 1}: +Input: ${JSON.stringify(ex.input, null, 2)} +Output: ${JSON.stringify(ex.output, null, 2)} +Explanation: ${ex.explanation} + `) + .join('\n'); + + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Examples for ${action.name}:\n${exampleText}` + } + } + ] + }; + } + ); + } + } + + return server; +} + +/** + * Helper to start the MCP server with stdio transport + */ +export async function startMcpServer( + actions: Record, + solanaAgentKit: SolanaAgentKit, + options: { + name: string; + version: string; + } +) { + const server = createMcpServer(actions, solanaAgentKit, options); + console.log("MCP server created"); + const transport = new StdioServerTransport(); + console.log("Stdio transport created"); + await server.connect(transport); + console.log("MCP server started"); + return server; +} + +/** + * Example usage: + * + * import { ACTIONS } from "./actions"; + * import { startMcpServer } from "./mcpWrapper"; + * + * const solanaAgentKit = new SolanaAgentKit(); + * + * startMcpServer(ACTIONS, solanaAgentKit, { + * name: "solana-actions", + * version: "1.0.0" + * }); + */ \ No newline at end of file diff --git a/src/utils/zodToMCPSchema.ts b/src/utils/zodToMCPSchema.ts new file mode 100644 index 00000000..e0068027 --- /dev/null +++ b/src/utils/zodToMCPSchema.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; + +// Define the raw shape type that MCP tools expect +export type MCPSchemaShape = { + [key: string]: z.ZodType; +}; + +// Type guards for Zod schema types +function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional { + return schema instanceof z.ZodOptional; +} + +function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject { + return schema instanceof z.ZodObject; +} + +/** + * Converts a Zod object schema to a flat shape for MCP tools + * @param schema The Zod schema to convert + * @returns A flattened schema shape compatible with MCP tools + * @throws Error if the schema is not an object type + */ +export function zodToMCPShape(schema: z.ZodTypeAny): MCPSchemaShape { + if (!isZodObject(schema)) { + throw new Error("MCP tools require an object schema at the top level"); + } + + const shape = schema.shape; + const result: MCPSchemaShape = {}; + + for (const [key, value] of Object.entries(shape)) { + // If it's an optional field, get the underlying type + result[key] = isZodOptional(value as any) ? (value as any).unwrap() : value; + } + + return result; +} + +// Example usage: +/* +const exampleSchema = z.object({ + name: z.string(), + age: z.number().optional(), + isActive: z.boolean() +}); + +const mcpShape = zodToMCPShape(exampleSchema); +*/ \ No newline at end of file diff --git a/test/mcp.ts b/test/mcp.ts new file mode 100644 index 00000000..9d6b6e1a --- /dev/null +++ b/test/mcp.ts @@ -0,0 +1,21 @@ +import { startMcpServer } from "../src/mcp"; +import { ACTIONS } from "../src/actions"; +import { SolanaAgentKit } from "../src/agent"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +const agent = new SolanaAgentKit( + process.env.SOLANA_PRIVATE_KEY!, + process.env.RPC_URL!, + { + OPENAI_API_KEY: process.env.OPENAI_API_KEY!, + }, + ); + + +const finalActions = { + GET : ACTIONS.GET_ASSET_ACTION +} + +startMcpServer(finalActions, agent, { name: "solana-agent", version: "0.0.1" }); From 9629960022958cc9d13cbf69c1cb5b1caad1b1f5 Mon Sep 17 00:00:00 2001 From: aryan Date: Sun, 16 Feb 2025 15:11:19 -0700 Subject: [PATCH 2/3] feat: mcp --- src/mcp/index.ts | 39 ++++++++++++++++++++----------------- src/utils/zodToMCPSchema.ts | 26 +++++++++++-------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/mcp/index.ts b/src/mcp/index.ts index d35726fc..800ebff7 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -24,13 +24,14 @@ export function createMcpServer( // Convert each action to an MCP tool for (const [key, action] of Object.entries(actions)) { + const { result, keys } = zodToMCPShape(action.schema); server.tool( action.name, action.description, - zodToMCPShape(action.schema), + result, async (params) => { try { - // Execute the action handler + // Execute the action handler with the params directly const result = await action.handler(solanaAgentKit, params); // Format the result as MCP tool response @@ -43,6 +44,7 @@ export function createMcpServer( ] }; } catch (error) { + console.error("error", error); // Handle errors in MCP format return { isError: true, @@ -98,9 +100,24 @@ Explanation: ${ex.explanation} return server; } - /** * Helper to start the MCP server with stdio transport + * + * @param actions - The actions to expose to the MCP server + * @param solanaAgentKit - The Solana agent kit + * @param options - The options for the MCP server + * @returns The MCP server + * @throws Error if the MCP server fails to start + * @example + * import { ACTIONS } from "./actions"; + * import { startMcpServer } from "./mcpWrapper"; + * + * const solanaAgentKit = new SolanaAgentKit(); + * + * startMcpServer(ACTIONS, solanaAgentKit, { + * name: "solana-actions", + * version: "1.0.0" + * }); */ export async function startMcpServer( actions: Record, @@ -117,18 +134,4 @@ export async function startMcpServer( await server.connect(transport); console.log("MCP server started"); return server; -} - -/** - * Example usage: - * - * import { ACTIONS } from "./actions"; - * import { startMcpServer } from "./mcpWrapper"; - * - * const solanaAgentKit = new SolanaAgentKit(); - * - * startMcpServer(ACTIONS, solanaAgentKit, { - * name: "solana-actions", - * version: "1.0.0" - * }); - */ \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/zodToMCPSchema.ts b/src/utils/zodToMCPSchema.ts index e0068027..5a6b3567 100644 --- a/src/utils/zodToMCPSchema.ts +++ b/src/utils/zodToMCPSchema.ts @@ -1,3 +1,4 @@ + import { z } from "zod"; // Define the raw shape type that MCP tools expect @@ -11,7 +12,11 @@ function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional { } function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject { - return schema instanceof z.ZodObject; + // Check both instanceof and the typeName property + return ( + schema instanceof z.ZodObject || + (schema?._def?.typeName === 'ZodObject') + ); } /** @@ -20,7 +25,7 @@ function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject { * @returns A flattened schema shape compatible with MCP tools * @throws Error if the schema is not an object type */ -export function zodToMCPShape(schema: z.ZodTypeAny): MCPSchemaShape { +export function zodToMCPShape(schema: z.ZodTypeAny): { result: MCPSchemaShape, keys: string[] } { if (!isZodObject(schema)) { throw new Error("MCP tools require an object schema at the top level"); } @@ -29,20 +34,11 @@ export function zodToMCPShape(schema: z.ZodTypeAny): MCPSchemaShape { const result: MCPSchemaShape = {}; for (const [key, value] of Object.entries(shape)) { - // If it's an optional field, get the underlying type result[key] = isZodOptional(value as any) ? (value as any).unwrap() : value; } - return result; + return { + result, + keys: Object.keys(result) + }; } - -// Example usage: -/* -const exampleSchema = z.object({ - name: z.string(), - age: z.number().optional(), - isActive: z.boolean() -}); - -const mcpShape = zodToMCPShape(exampleSchema); -*/ \ No newline at end of file From 2ad519e109527f1d9cf1415b41bc8b9ac81a329c Mon Sep 17 00:00:00 2001 From: aryan Date: Sun, 16 Feb 2025 15:17:07 -0700 Subject: [PATCH 3/3] feat: add exports --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 5199735a..78e67e77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ import { SolanaAgentKit } from "./agent"; import { createSolanaTools } from "./langchain"; import { createSolanaTools as createVercelAITools } from "./vercel-ai"; +import { startMcpServer , createMcpServer } from "./mcp"; -export { SolanaAgentKit, createSolanaTools, createVercelAITools }; +export { SolanaAgentKit, createSolanaTools, createVercelAITools, startMcpServer , createMcpServer }; // Optional: Export types that users might need export * from "./types"; @@ -10,3 +11,6 @@ export * from "./types"; // Export action system export { ACTIONS } from "./actions"; export * from "./utils/actionExecutor"; + +// Export MCP server +export * from "./utils/zodToMCPSchema";