Skip to content

Commit ab4cde7

Browse files
committed
feat: add shared MCP server base patterns
1 parent f89a1f9 commit ab4cde7

File tree

1 file changed

+263
-0
lines changed

1 file changed

+263
-0
lines changed

shared/patterns/mcp-server-base.ts

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/**
2+
* Base patterns and utilities for MCP server development
3+
* This provides common functionality that all MCP servers can use
4+
*/
5+
6+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8+
import {
9+
CallToolRequestSchema,
10+
ListToolsRequestSchema,
11+
type CallToolRequest,
12+
type ListToolsRequest,
13+
} from '@modelcontextprotocol/sdk/types.js';
14+
15+
export interface MCPTool {
16+
name: string;
17+
description: string;
18+
inputSchema: object;
19+
execute: (input: any) => Promise<any>;
20+
}
21+
22+
export interface MCPServerConfig {
23+
name: string;
24+
version: string;
25+
description?: string;
26+
tools: MCPTool[];
27+
enableCLI?: boolean;
28+
cliCommands?: Record<string, (args: any) => Promise<void>>;
29+
}
30+
31+
/**
32+
* Base class for MCP servers with common functionality
33+
*/
34+
export class MCPServerBase {
35+
protected server: Server;
36+
protected config: MCPServerConfig;
37+
protected tools: Map<string, MCPTool> = new Map();
38+
39+
constructor(config: MCPServerConfig) {
40+
this.config = config;
41+
42+
this.server = new Server(
43+
{
44+
name: config.name,
45+
version: config.version,
46+
},
47+
{
48+
capabilities: {
49+
tools: {},
50+
},
51+
}
52+
);
53+
54+
// Register tools
55+
config.tools.forEach(tool => {
56+
this.tools.set(tool.name, tool);
57+
});
58+
59+
this.setupHandlers();
60+
}
61+
62+
/**
63+
* Set up MCP request handlers
64+
*/
65+
private setupHandlers(): void {
66+
// List tools handler
67+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
68+
tools: Array.from(this.tools.values()).map(tool => ({
69+
name: tool.name,
70+
description: tool.description,
71+
inputSchema: tool.inputSchema,
72+
})),
73+
}));
74+
75+
// Call tool handler
76+
this.server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
77+
const { name, arguments: args } = request.params;
78+
79+
const tool = this.tools.get(name);
80+
if (!tool) {
81+
throw new Error(`Unknown tool: ${name}`);
82+
}
83+
84+
try {
85+
const result = await tool.execute(args);
86+
return {
87+
content: [
88+
{
89+
type: 'text',
90+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
91+
},
92+
],
93+
};
94+
} catch (error) {
95+
const errorMessage = error instanceof Error ? error.message : String(error);
96+
return {
97+
content: [
98+
{
99+
type: 'text',
100+
text: `Error executing ${name}: ${errorMessage}`,
101+
},
102+
],
103+
isError: true,
104+
};
105+
}
106+
});
107+
}
108+
109+
/**
110+
* Add a tool to the server
111+
*/
112+
addTool(tool: MCPTool): void {
113+
this.tools.set(tool.name, tool);
114+
}
115+
116+
/**
117+
* Remove a tool from the server
118+
*/
119+
removeTool(name: string): void {
120+
this.tools.delete(name);
121+
}
122+
123+
/**
124+
* Start the MCP server
125+
*/
126+
async start(): Promise<void> {
127+
// Check if running as CLI
128+
if (this.config.enableCLI && this.isRunningAsCLI()) {
129+
await this.runCLI();
130+
return;
131+
}
132+
133+
// Start as MCP server
134+
const transport = new StdioServerTransport();
135+
await this.server.connect(transport);
136+
}
137+
138+
/**
139+
* Check if running as CLI (not as MCP server)
140+
*/
141+
private isRunningAsCLI(): boolean {
142+
// Check for CLI-specific arguments or environment
143+
return process.argv.includes('--cli') ||
144+
process.argv.includes('cli') ||
145+
process.env.MCP_CLI_MODE === 'true';
146+
}
147+
148+
/**
149+
* Run in CLI mode
150+
*/
151+
private async runCLI(): Promise<void> {
152+
if (!this.config.cliCommands) {
153+
console.error('CLI mode enabled but no CLI commands defined');
154+
process.exit(1);
155+
}
156+
157+
const command = process.argv[2];
158+
const cliHandler = this.config.cliCommands[command];
159+
160+
if (!cliHandler) {
161+
console.error(`Unknown command: ${command}`);
162+
console.error('Available commands:', Object.keys(this.config.cliCommands).join(', '));
163+
process.exit(1);
164+
}
165+
166+
try {
167+
await cliHandler(process.argv.slice(3));
168+
} catch (error) {
169+
console.error('CLI Error:', error instanceof Error ? error.message : String(error));
170+
process.exit(1);
171+
}
172+
}
173+
174+
/**
175+
* Create a tool with validation
176+
*/
177+
static createTool(config: {
178+
name: string;
179+
description: string;
180+
inputSchema: object;
181+
execute: (input: any) => Promise<any>;
182+
validate?: (input: any) => boolean | string;
183+
}): MCPTool {
184+
return {
185+
name: config.name,
186+
description: config.description,
187+
inputSchema: config.inputSchema,
188+
execute: async (input: any) => {
189+
// Run validation if provided
190+
if (config.validate) {
191+
const validation = config.validate(input);
192+
if (validation !== true) {
193+
throw new Error(typeof validation === 'string' ? validation : 'Invalid input');
194+
}
195+
}
196+
197+
return config.execute(input);
198+
},
199+
};
200+
}
201+
202+
/**
203+
* Helper to create error responses
204+
*/
205+
protected createErrorResponse(message: string): { content: Array<{ type: string; text: string }>; isError: boolean } {
206+
return {
207+
content: [{ type: 'text', text: `Error: ${message}` }],
208+
isError: true,
209+
};
210+
}
211+
212+
/**
213+
* Helper to create success responses
214+
*/
215+
protected createSuccessResponse(data: any): { content: Array<{ type: string; text: string }> } {
216+
return {
217+
content: [
218+
{
219+
type: 'text',
220+
text: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
221+
},
222+
],
223+
};
224+
}
225+
}
226+
227+
/**
228+
* Utility function to create and start an MCP server
229+
*/
230+
export async function createMCPServer(config: MCPServerConfig): Promise<void> {
231+
const server = new MCPServerBase(config);
232+
await server.start();
233+
}
234+
235+
/**
236+
* Decorator for tool methods (for class-based tool definitions)
237+
*/
238+
export function tool(name: string, description: string, inputSchema: object) {
239+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
240+
// Store tool metadata
241+
if (!target.constructor._tools) {
242+
target.constructor._tools = [];
243+
}
244+
245+
target.constructor._tools.push({
246+
name,
247+
description,
248+
inputSchema,
249+
execute: descriptor.value,
250+
});
251+
};
252+
}
253+
254+
/**
255+
* Helper to extract tools from a class decorated with @tool
256+
*/
257+
export function extractToolsFromClass(instance: any): MCPTool[] {
258+
const tools = instance.constructor._tools || [];
259+
return tools.map((tool: any) => ({
260+
...tool,
261+
execute: tool.execute.bind(instance),
262+
}));
263+
}

0 commit comments

Comments
 (0)