Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"^node:(.*)$",
"<BUILTIN_MODULES>",
"",
"<THIRD_PARTY_MODULES>",
"^[^@./].*$",
"",
"^@(?!smythos/)[^/]+/.*$",
"",
"^@smythos/(.*)$",
"",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
},
"dependencies": {
"@faker-js/faker": "^9.2.0",
"@modelcontextprotocol/sdk": "^1.22.0",
"@smythos/sdk": "^1.3.20",
"axios": "^1.13.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export * from './roles/agent/AgentRequestHandler';
export * from './roles/alexa/Alexa.role';
export * from './roles/alexa/Alexa.service';
export * from './roles/chatgpt/ChatGPT.role';
export * from './roles/mcp/MCP.role';
export * from './roles/mcp/MCP.service';
export * from './roles/openai/Chat.service';
export * from './roles/openai/chat.validation';
export * from './roles/openai/OpenAI.role';
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts.bak
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export * from './roles/agent/AgentRequestHandler';
export * from './roles/alexa/Alexa.role';
export * from './roles/alexa/Alexa.service';
export * from './roles/chatgpt/ChatGPT.role';
export * from './roles/mcp/MCP.role';
export * from './roles/mcp/MCP.service';
export * from './roles/openai/Chat.service';
export * from './roles/openai/chat.validation';
export * from './roles/openai/OpenAI.role';
Expand Down
187 changes: 187 additions & 0 deletions src/roles/mcp/MCP.role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import express from 'express';

import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js';

import { AgentProcess, ConnectorService, Logger } from '@smythos/sdk/core';

import AgentLoader from '@/middlewares/AgentLoader.mw';
import { BaseRole } from '@/roles/Base.role';

import { extractMCPToolSchema, formatMCPSchemaProperties } from './MCP.service';

// TODO:
// * 1. Replace Deprecated Server with McpServer
// * 2. Implement StreamableHTTPServerTransport, and keep the SSEServerTransport for backward compatibility

const console = Logger('Role: MCP');

const clientTransports = new Map<string, { transport: SSEServerTransport; server: McpServer }>();

export class MCPRole extends BaseRole {
/**
* Creates a new MCPRole instance.
* @param middlewares - The custom middlewares to apply to the role on top of the default middlewares.
* @param options - The options for the role. Defaults to an empty object.
*/
constructor(middlewares: express.RequestHandler[] = [], options: Record<string, unknown> = {}) {
super(middlewares, options);
}

public async mount(router: express.Router) {
const middlewares = [AgentLoader, ...this.middlewares];

router.get('/sse', middlewares, async (req: express.Request, res: express.Response) => {
try {
const agentData = req._agentData;

// #region // TODO: we remove this if authentication enabled for MCP
if (agentData?.auth?.method && agentData?.auth?.method !== 'none') {
return res.status(400).send({ error: 'Agents with authentication enabled are not supported for MCP' });
}
// #endregion

const agentDataConnector = ConnectorService.getAgentDataConnector();
const openAPISpec = await agentDataConnector.getOpenAPIJSON(agentData, 'localhost', 'latest', true);

// Server implementation
const server = new McpServer(
{
name: openAPISpec.info.title,
version: openAPISpec.info.version,
},
{
capabilities: {
tools: {},
},
},
);
req.on('error', (error: unknown) => {
console.error('Error:', error);
// server.close();
});

// Handle client disconnect
req.on('close', () => {
console.log('Client disconnected');
clientTransports.delete(transport.sessionId);
// server.close();
});

server.onerror = (error: unknown) => {
console.error('Server error:', error);
// server.close();
};

server.onclose = async () => {
console.log('Server closing');
// await server.close();
// process.exit(0);
};
// Extract available endpoints and their methods
const tools: Tool[] = Object.entries(openAPISpec.paths).map(([path, methods]) => {
const method = Object.keys(methods)[0];
const endpoint = path.split('/api/')[1];
const operation = methods[method];
const schema = extractMCPToolSchema(operation, method);
const properties = formatMCPSchemaProperties(schema);

return {
name: endpoint,
description:
operation.summary ||
`Endpoint that handles ${method.toUpperCase()} requests to ${endpoint}. ` + `${schema?.description || ''}`,
inputSchema: {
type: 'object',
properties: properties,
required: schema?.required || [],
},
};
});

// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;

if (!args) {
throw new Error('No arguments provided');
}

// Find the matching tool from our tools array
const tool = tools.find((t) => t.name === name);
if (!tool) {
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
}

try {
// Extract method and path from OpenAPI spec
const pathEntry = Object.entries(openAPISpec.paths).find(([path]) => path.split('/api/')[1] === name);
if (!pathEntry) {
throw new Error(`Could not find path for tool: ${name}`);
}

const [path, methods] = pathEntry;
const method = Object.keys(methods)[0];

// Process the request through the agent
const result = await AgentProcess.load(agentData).run({
method: method,
path: path,
body: args,
});

return {
content: [{ type: 'text', text: JSON.stringify(result) }],
isError: false,
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error processing request: ${error.message}` }],
isError: true,
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});

const transport = new SSEServerTransport('/emb/mcp/message', res);
await server.connect(transport);

clientTransports.set(transport.sessionId, { transport, server });
console.log('Generated sessionId', transport.sessionId);
console.log('MCP Server running on sse');
} catch (error: unknown) {
console.error(error);
return res.status(500).send({ error: (error as Error).message });
}
});

router.post('/message', async (req: express.Request, res: express.Response) => {
const sessionId = req.query.sessionId;
console.log('Received sessionId', sessionId);
const transport = clientTransports.get(sessionId as string)?.transport;
if (!transport) {
return res.status(404).send({ error: 'Transport not found' });
}
await transport.handlePostMessage(req, res, req.body);
});
}
}
63 changes: 63 additions & 0 deletions src/roles/mcp/MCP.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const MCP_SETTINGS_KEY = 'mcp';

export const isMcpEnabled = (agentData: any, agentSettings) => {
if (agentData.usingTestDomain) {
return true;
}
const mcpSettings = agentSettings?.get(MCP_SETTINGS_KEY);
let isEnabled = false;
if (mcpSettings) {
try {
const parsedMcpSettings = JSON.parse(mcpSettings);
isEnabled = typeof parsedMcpSettings === 'boolean' ? parsedMcpSettings : parsedMcpSettings?.isEnabled;
} catch (error) {
isEnabled = false;
}
}
return isEnabled;
};

export function extractMCPToolSchema(jsonSpec: any, method: string) {
if (method.toLowerCase() === 'get') {
const schema = jsonSpec?.parameters;
if (!schema) return {};

const properties = {};
const required = [];

schema.forEach((param) => {
if (param.in === 'query') {
properties[param.name] = param.schema;
if (param.required) {
required.push(param.name);
}
}
});

return {
type: 'object',
properties,
required,
};
}
const schema = jsonSpec?.requestBody?.content?.['application/json']?.schema;
return schema;
}

export function formatMCPSchemaProperties(schema: any) {
const properties = schema?.properties || {};
for (const property in properties) {
const propertySchema = properties[property];

if (propertySchema.type === 'array') {
properties[property] = {
type: 'array',
items: {
type: ['string', 'number', 'boolean', 'object', 'array'],
},
};
}
}
return properties;
}