Skip to content

Commit

Permalink
Feat/mcp (#283)
Browse files Browse the repository at this point in the history
# Pull Request Description

This PR adds helpers for creating a MPC server 

### Testing

- [x] Have tested the changes locally
  • Loading branch information
thearyanag authored Feb 16, 2025
2 parents b1e54e7 + 7d4df9d commit c3127a3
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 1 deletion.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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";

// Export action system
export { ACTIONS } from "./actions";
export * from "./utils/actionExecutor";

// Export MCP server
export * from "./utils/zodToMCPSchema";
137 changes: 137 additions & 0 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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<string, Action>,
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)) {

Check warning on line 26 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / check

'key' is assigned a value but never used
const { result, keys } = zodToMCPShape(action.schema);

Check warning on line 27 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / check

'keys' is assigned a value but never used
server.tool(
action.name,
action.description,
result,
async (params) => {
try {
// Execute the action handler with the params directly
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) {
console.error("error", 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
*
* @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<string, Action>,
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");

Check warning on line 135 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / check

Unexpected console statement
return server;
}

Check warning on line 137 in src/mcp/index.ts

View workflow job for this annotation

GitHub Actions / check

Unexpected console statement
44 changes: 44 additions & 0 deletions src/utils/zodToMCPSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

import { z } from "zod";

// Define the raw shape type that MCP tools expect
export type MCPSchemaShape = {
[key: string]: z.ZodType<any>;
};

// Type guards for Zod schema types
function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> {
return schema instanceof z.ZodOptional;
}

function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> {
// Check both instanceof and the typeName property
return (
schema instanceof z.ZodObject ||
(schema?._def?.typeName === '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): { result: MCPSchemaShape, keys: string[] } {
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)) {
result[key] = isZodOptional(value as any) ? (value as any).unwrap() : value;
}

return {
result,
keys: Object.keys(result)
};
}
21 changes: 21 additions & 0 deletions test/mcp.ts
Original file line number Diff line number Diff line change
@@ -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" });

0 comments on commit c3127a3

Please sign in to comment.