Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"build:watch": "tsc --watch",
"dev:stdio": "tsx src/stdio.ts",
"dev:http": "tsx watch --env-file=.env src/http.ts",
"dev:centralized": "tsx watch --env-file=.env src/centralized.ts",
"inspector": "npx @modelcontextprotocol/inspector",
"test": "echo \"Error: no test specified\" && exit 1",
"mintRlI": "tsx --env-file=.env ./src/bin/mintRLINft.ts"
Expand Down
75 changes: 75 additions & 0 deletions packages/mcp/src/appDefinitions/196/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"id": "196",
"version": "1",
"name": "Uniswap Swap",
"description": "This app offers tools to approve ERC20 token allowances and perform swaps on uniswap",
"tools": {
"QmPZ46EiurxMb7DmE9McFyzHfg2B6ZGEERui2tnNNX7cky": {
"name": "erc20-approval",
"description": "Allow an ERC20 token spending, up to a limit, to the Uniswap v3 Router contract. This is necessary to make trades on Uniswap.",
"parameters": [
{
"name": "chainId",
"type": "string",
"description": "The chain ID to execute the transaction on. For example: 8453 for Base."
},
{
"name": "tokenIn",
"type": "string",
"description": "ERC20 Token address to approve. For example 0x4200000000000000000000000000000000000006 for WETH on Base."
},
{
"name": "amountIn",
"type": "string",
"description": "Amount of tokenIn to approve. For example 0.00001 for 0.00001 WETH."
},
{
"name": "pkpEthAddress",
"type": "string",
"description": "The delegator's PKP address that will execute the swap. For example 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045."
},
{
"name": "rpcUrl",
"type": "string",
"description": "The RPC URL to use for the transaction"
}
]
},
"QmZbh52JYnutuFURnpwfywfiiHuFoJpqFyFzNiMtbiDNkK": {
"name": "uniswap-swap",
"description": "Executes a swap in Uniswap selling an specific amount of the input token to get another token. The necessary allowance for the input token must be approved for the Uniswap v3 Router contract.",
"parameters": [
{
"name": "chainId",
"type": "string",
"description": "The chain ID to execute the transaction on. For example: 8453 for Base."
},
{
"name": "tokenIn",
"type": "string",
"description": "ERC20 Token address to sell. For example 0x4200000000000000000000000000000000000006 for WETH on Base."
},
{
"name": "amountIn",
"type": "string",
"description": "Amount of tokenIn to sell. For example 0.00001 for 0.00001 WETH."
},
{
"name": "tokenOut",
"type": "string",
"description": "ERC20 Token address to buy. For example 0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C for SPX600 on Base."
},
{
"name": "pkpEthAddress",
"type": "string",
"description": "The delegator's PKP address that will execute the swap. For example 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045."
},
{
"name": "rpcUrl",
"type": "string",
"description": "The RPC URL to use for the transaction"
}
]
}
}
}
135 changes: 135 additions & 0 deletions packages/mcp/src/centralized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env node
/**
* Bundled HTTP server implementation for Vincent MCP
*
* This module provides an HTTP server implementation for the Model Context Protocol (MCP)
* using Express. It creates a streamable HTTP server that can handle MCP requests
* and maintain session state across multiple requests.
*
* The server loads a Vincent application definition from a JSON file on demand and creates
* an MCP server with extended capabilities for that application. It then exposes
* the server via HTTP endpoints that follow the MCP protocol.
*
* @module http
* @category Vincent MCP
*/

import { randomUUID } from 'node:crypto';

import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import express, { Request, Response } from 'express';

import { getVincentDelegateeSigner } from './delegatees';
import { env } from './env';
import { getVincentAppDef } from './registry';
import { getServer } from './server';

const MCP_URL = '/:appId/:appVersion/mcp';

const { HTTP_PORT } = env;

const app = express();
app.use(express.json());

// In-memory store for transports
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

app.post(MCP_URL, async (req: Request, res: Response) => {
try {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = transport;
},
});

// Cleanup transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};

const authorization = (req.headers['authorization'] as string) || '';
const [, delegateePrivateKey] = authorization.split(' ');
if (!delegateePrivateKey) {
throw new Error('No delegatee private key provided');
}

const { appId, appVersion } = req.params;
const delegateeSigner = getVincentDelegateeSigner(delegateePrivateKey);
const vincentAppDef = await getVincentAppDef(appId, appVersion);
if (!delegateeSigner) {
throw new Error(
`No delegatee signer found for app ${appId}. App must be registered with a delegatee private key before it can be used.`,
);
}

const server = getServer(vincentAppDef, delegateeSigner);
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}

// Handle the request
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: (error as Error).message || 'Internal Server Error',
},
id: null,
});
}
});

/**
* Handles GET and DELETE requests for MCP sessions
*
* This function processes requests that require an existing session,
* such as GET requests for streaming responses or DELETE requests to
* terminate a session. It validates the session ID and delegates the
* request handling to the appropriate transport.
*
* @param req - The Express request object
* @param res - The Express response object
* @internal
*/
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}

const transport = transports[sessionId];
await transport.handleRequest(req, res);
};

app.get(MCP_URL, handleSessionRequest);
app.delete(MCP_URL, handleSessionRequest);

app.listen(HTTP_PORT, () => {
console.log(`Centralized Vincent MCP Server listening on port ${HTTP_PORT}`);
});
11 changes: 11 additions & 0 deletions packages/mcp/src/delegatees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LIT_EVM_CHAINS } from '@lit-protocol/constants';
import { ethers } from 'ethers';

export function getVincentDelegateeSigner(delegateePrivateKey: string) {
const delegateeSigner = new ethers.Wallet(
delegateePrivateKey,
new ethers.providers.StaticJsonRpcProvider(LIT_EVM_CHAINS.yellowstone.rpcUrls[0]),
);

return delegateeSigner;
}
2 changes: 1 addition & 1 deletion packages/mcp/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const env = createEnv({
HTTP_PORT: z.coerce.number().default(3000),
PUBKEY_ROUTER_DATIL_CONTRACT: z.string(),
VINCENT_APP_JSON_DEFINITION: z.string(),
VINCENT_DELEGATEE_PRIVATE_KEY: z.string(),
VINCENT_DATIL_CONTRACT: z.string(),
VINCENT_DELEGATEE_PRIVATE_KEY: z.string(),
},
});
9 changes: 6 additions & 3 deletions packages/mcp/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import express, { Request, Response } from 'express';

import { getVincentDelegateeSigner } from './delegatees';
import { env } from './env';
import { getServer } from './server';

const { HTTP_PORT, VINCENT_APP_JSON_DEFINITION } = env;
const { HTTP_PORT, VINCENT_APP_JSON_DEFINITION, VINCENT_DELEGATEE_PRIVATE_KEY } = env;
const { VincentAppDefSchema } = mcp;

const vincentAppJson = fs.readFileSync(VINCENT_APP_JSON_DEFINITION, { encoding: 'utf8' });
Expand Down Expand Up @@ -61,7 +62,9 @@ app.post('/mcp', async (req: Request, res: Response) => {
}
};

const server = getServer(vincentAppDef);
// In local HTTP configuration, the signer is defined with VINCENT_DELEGATEE_PRIVATE_KEY env variable
const delegateeSigner = getVincentDelegateeSigner(VINCENT_DELEGATEE_PRIVATE_KEY);
const server = getServer(vincentAppDef, delegateeSigner);
await server.connect(transport);
} else {
// Invalid request
Expand All @@ -84,7 +87,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Internal Server Error',
message: (error as Error).message || 'Internal Server Error',
},
id: null,
});
Expand Down
11 changes: 11 additions & 0 deletions packages/mcp/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mcp } from '@lit-protocol/vincent-sdk';
import fs from 'node:fs/promises';

const { VincentAppDefSchema } = mcp;

export async function getVincentAppDef(appId: string, appVersion: string) {
const vincentAppDefLocation = `${process.cwd()}/src/appDefinitions/${appId}/${appVersion}.json`;

const vincentAppJson = await fs.readFile(vincentAppDefLocation, { encoding: 'utf8' });
return VincentAppDefSchema.parse(JSON.parse(vincentAppJson));
}
18 changes: 7 additions & 11 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,25 @@
* @category Vincent MCP
*/

import { LIT_EVM_CHAINS } from '@lit-protocol/constants';
import { mcp } from '@lit-protocol/vincent-sdk';
import { ethers } from 'ethers';

import { env } from './env';
import { extendVincentServer } from './extensions';

const { VINCENT_DELEGATEE_PRIVATE_KEY } = env;
const { getVincentAppServer } = mcp;

/**
* Creates an extended MCP server for a Vincent application
*
* This function creates an MCP server for a Vincent application and extends it
* with additional functionality. It uses the delegatee private key from the
* with additional functionality. It assigns a signer using the delegatee private key from the
* environment to create a signer that can execute Vincent tools on behalf of users.
*
* The server is created using the Vincent SDK's `getVincentAppServer` function
* and then extended with additional capabilities using the `extendVincentServer` function.
*
* @param vincentAppDef - The Vincent application definition containing the tools to register
* @param delegateeSigner - The Ethereum signer used to execute Vincent tools
* @returns A configured and extended MCP server instance
*
* @example
Expand All @@ -48,20 +46,18 @@ const { getVincentAppServer } = mcp;
* }
* };
*
* // Create a signer
* const delegateeSigner = getVincentDelegateeSigner(VINCENT_DELEGATEE_PRIVATE_KEY);
*
* // Create the extended MCP server
* const server = getServer(appDef);
* const server = getServer(appDef, delegateeSigner);
*
* // Add transport to expose the server
* const stdio = new StdioServerTransport();
* await server.connect(stdio);
* ```
*/
export function getServer(vincentAppDef: mcp.VincentAppDef) {
const delegateeSigner = new ethers.Wallet(
VINCENT_DELEGATEE_PRIVATE_KEY,
new ethers.providers.StaticJsonRpcProvider(LIT_EVM_CHAINS.yellowstone.rpcUrls[0]),
);

export function getServer(vincentAppDef: mcp.VincentAppDef, delegateeSigner: ethers.Signer) {
const server = getVincentAppServer(delegateeSigner, vincentAppDef);
extendVincentServer(server, vincentAppDef, delegateeSigner);

Expand Down
7 changes: 5 additions & 2 deletions packages/mcp/src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import fs from 'node:fs';
import { mcp } from '@lit-protocol/vincent-sdk';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

import { getVincentDelegateeSigner } from './delegatees';
import { env } from './env';
import { getServer } from './server';

const { VINCENT_APP_JSON_DEFINITION } = env;
const { VINCENT_APP_JSON_DEFINITION, VINCENT_DELEGATEE_PRIVATE_KEY } = env;
const { VincentAppDefSchema } = mcp;

/**
Expand All @@ -45,7 +46,9 @@ async function main() {
const vincentAppJson = fs.readFileSync(VINCENT_APP_JSON_DEFINITION, { encoding: 'utf8' });
const vincentAppDef = VincentAppDefSchema.parse(JSON.parse(vincentAppJson));

const server = getServer(vincentAppDef);
// In local STDIO configuration, the signer is defined with VINCENT_DELEGATEE_PRIVATE_KEY env variable
const delegateeSigner = getVincentDelegateeSigner(VINCENT_DELEGATEE_PRIVATE_KEY);
const server = getServer(vincentAppDef, delegateeSigner);
await server.connect(stdioTransport);
console.error('Vincent MCP Server running in stdio mode'); // console.log is used for messaging the parent process
}
Expand Down
Loading