Files
gh-jezweb-claude-skills-ski…/templates/tool-server.ts
2025-11-30 08:25:43 +08:00

284 lines
7.0 KiB
TypeScript

/**
* Tool-Server MCP Template
*
* An MCP server focused on exposing multiple tools for LLM use.
* Examples include API integrations, calculations, and data transformations.
*
* Setup:
* 1. npm install @modelcontextprotocol/sdk hono zod
* 2. npm install -D @cloudflare/workers-types wrangler typescript
* 3. Add environment variables to wrangler.jsonc or .dev.vars
* 4. wrangler deploy
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Hono } from 'hono';
import { z } from 'zod';
type Env = {
// API keys for external services
WEATHER_API_KEY?: string;
// Database (optional)
DB?: D1Database;
// Cache (optional)
CACHE?: KVNamespace;
};
const server = new McpServer({
name: 'tool-server',
version: '1.0.0'
});
// Tool 1: Weather API Integration
server.registerTool(
'get-weather',
{
description: 'Fetches current weather data for a city using OpenWeatherMap API',
inputSchema: z.object({
city: z.string().describe('City name (e.g., "London", "New York")'),
units: z.enum(['metric', 'imperial']).default('metric').describe('Temperature units')
})
},
async ({ city, units }, env) => {
if (!env.WEATHER_API_KEY) {
return {
content: [{
type: 'text',
text: 'Error: WEATHER_API_KEY not configured'
}],
isError: true
};
}
try {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${env.WEATHER_API_KEY}`
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.statusText}`);
}
const data = await response.json() as {
main: { temp: number; feels_like: number; humidity: number };
weather: Array<{ description: string }>;
name: string;
};
const tempUnit = units === 'metric' ? '°C' : '°F';
return {
content: [{
type: 'text',
text: `Weather in ${data.name}:
- Temperature: ${data.main.temp}${tempUnit}
- Feels like: ${data.main.feels_like}${tempUnit}
- Humidity: ${data.main.humidity}%
- Conditions: ${data.weather[0].description}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Failed to fetch weather: ${(error as Error).message}`
}],
isError: true
};
}
}
);
// Tool 2: Calculator
server.registerTool(
'calculate',
{
description: 'Performs mathematical calculations',
inputSchema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide', 'power', 'sqrt']).describe('Mathematical operation'),
a: z.number().describe('First number'),
b: z.number().optional().describe('Second number (not required for sqrt)')
})
},
async ({ operation, a, b }) => {
let result: number;
try {
switch (operation) {
case 'add':
result = a + (b || 0);
break;
case 'subtract':
result = a - (b || 0);
break;
case 'multiply':
result = a * (b || 1);
break;
case 'divide':
if (b === 0) throw new Error('Division by zero');
result = a / (b || 1);
break;
case 'power':
result = Math.pow(a, b || 2);
break;
case 'sqrt':
if (a < 0) throw new Error('Cannot take square root of negative number');
result = Math.sqrt(a);
break;
}
return {
content: [{
type: 'text',
text: `Result: ${result}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Calculation error: ${(error as Error).message}`
}],
isError: true
};
}
}
);
// Tool 3: Text Analysis
server.registerTool(
'analyze-text',
{
description: 'Analyzes text and returns statistics',
inputSchema: z.object({
text: z.string().describe('Text to analyze')
})
},
async ({ text }) => {
const words = text.trim().split(/\s+/);
const chars = text.length;
const charsNoSpaces = text.replace(/\s/g, '').length;
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
const paragraphs = text.split(/\n\n+/).filter(p => p.trim().length > 0).length;
return {
content: [{
type: 'text',
text: `Text Analysis:
- Words: ${words.length}
- Characters: ${chars}
- Characters (no spaces): ${charsNoSpaces}
- Sentences: ${sentences}
- Paragraphs: ${paragraphs}
- Average word length: ${(charsNoSpaces / words.length).toFixed(2)} chars`
}]
};
}
);
// Tool 4: JSON Formatter
server.registerTool(
'format-json',
{
description: 'Formats and validates JSON data',
inputSchema: z.object({
json: z.string().describe('JSON string to format'),
indent: z.number().default(2).describe('Number of spaces for indentation')
})
},
async ({ json, indent }) => {
try {
const parsed = JSON.parse(json);
const formatted = JSON.stringify(parsed, null, indent);
return {
content: [{
type: 'text',
text: `Formatted JSON:\n\`\`\`json\n${formatted}\n\`\`\``
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Invalid JSON: ${(error as Error).message}`
}],
isError: true
};
}
}
);
// Tool 5: URL Parser
server.registerTool(
'parse-url',
{
description: 'Parses a URL and returns its components',
inputSchema: z.object({
url: z.string().url().describe('URL to parse')
})
},
async ({ url }) => {
try {
const parsed = new URL(url);
return {
content: [{
type: 'text',
text: `URL Components:
- Protocol: ${parsed.protocol}
- Host: ${parsed.host}
- Hostname: ${parsed.hostname}
- Port: ${parsed.port || 'default'}
- Pathname: ${parsed.pathname}
- Search: ${parsed.search || 'none'}
- Hash: ${parsed.hash || 'none'}
- Origin: ${parsed.origin}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Invalid URL: ${(error as Error).message}`
}],
isError: true
};
}
}
);
// HTTP endpoint setup
const app = new Hono<{ Bindings: Env }>();
app.get('/', (c) => {
return c.json({
name: 'tool-server',
version: '1.0.0',
tools: [
'get-weather',
'calculate',
'analyze-text',
'format-json',
'parse-url'
]
});
});
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
c.res.raw.on('close', () => transport.close());
// Pass env to server context
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
export default app;