UNPKG

tokyo-metro-mcp-server

Version:
397 lines 15.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import express from 'express'; import cors from 'cors'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; const server = new Server({ name: 'tokyo-metro-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); async function makeTokyoMetroRequest(apiUrl, body, limit) { const url = limit ? `${apiUrl}?limit=${limit}` : apiUrl; const response = await fetch(url, { method: 'POST', headers: { 'accept': 'application/json', 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'tokyo_metro_query', description: 'Query Tokyo Metro API with POST request and optional filtering', inputSchema: { type: 'object', properties: { api_url: { type: 'string', description: 'The Tokyo Metro API endpoint URL', }, columns: { type: 'array', items: { type: 'string', }, description: 'Specific columns to retrieve from the API', }, limit: { type: 'number', description: 'Maximum number of records to return', default: 100, }, search_conditions: { type: 'object', properties: { condition_relationship: { type: 'string', enum: ['and', 'or'], description: 'How to combine multiple search conditions', }, date_filters: { type: 'array', items: { type: 'object', properties: { column: { type: 'string', description: 'Column name to filter on', }, operator: { type: 'string', enum: ['eq', 'ne', 'gt', 'ge', 'lt', 'le'], description: 'Comparison operator', }, value: { type: 'string', description: 'Value to compare against', }, }, required: ['column', 'operator', 'value'], }, description: 'Date-based filter conditions', }, text_filters: { type: 'array', items: { type: 'object', properties: { column: { type: 'string', description: 'Column name to filter on', }, operator: { type: 'string', enum: ['eq', 'ne', 'partial'], description: 'Text comparison operator', }, value: { type: 'string', description: 'Value to compare against', }, }, required: ['column', 'operator', 'value'], }, description: 'Text-based filter conditions', }, }, description: 'Search conditions for filtering API results', }, }, required: ['api_url'], }, }, { name: 'get_sample_covid_data', description: 'Get sample COVID-19 data from Tokyo Metro API', inputSchema: { type: 'object', properties: { start_date: { type: 'string', description: 'Start date in YYYY/M/D format (e.g., 2021/1/1)', }, end_date: { type: 'string', description: 'End date in YYYY/M/D format (e.g., 2021/12/31)', }, limit: { type: 'number', description: 'Maximum number of records to return', default: 365, }, }, }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === 'tokyo_metro_query') { const { api_url, columns, limit, search_conditions } = args; let body; if (columns || search_conditions) { body = {}; if (columns) { body.column = columns; } if (search_conditions) { body.searchCondition = { conditionRelationship: search_conditions.condition_relationship || 'and', }; if (search_conditions.date_filters) { body.searchCondition.dateAndSearch = search_conditions.date_filters.map((filter) => ({ column: filter.column, relationship: filter.operator, condition: filter.value, })); } if (search_conditions.text_filters) { body.searchCondition.textAndSearch = search_conditions.text_filters.map((filter) => ({ column: filter.column, relationship: filter.operator, condition: filter.value, })); } } } const result = await makeTokyoMetroRequest(api_url, body, limit); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_sample_covid_data') { const { start_date, end_date, limit = 365 } = args; const apiUrl = 'https://service.api.metro.tokyo.lg.jp/api/t000001d0000000011-819fb24a2e74a5f2ea848d548c5cff7d-0/json'; let body = { column: [ '公表_年月日', '日別陽性者数', '7日間合計陽性者数', '7日間平均陽性者数', ], }; if (start_date || end_date) { body.searchCondition = { conditionRelationship: 'and', dateAndSearch: [], }; if (start_date) { body.searchCondition.dateAndSearch.push({ column: '公表_年月日', relationship: 'ge', condition: start_date, }); } if (end_date) { body.searchCondition.dateAndSearch.push({ column: '公表_年月日', relationship: 'le', condition: end_date, }); } } const result = await makeTokyoMetroRequest(apiUrl, body, limit); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // HTTP API endpoints for Mastra integration function createHttpServer() { const app = express(); const PORT = process.env.PORT || 3001; app.use(cors()); app.use(express.json()); // Debug middleware to log all requests app.use((req, res, next) => { console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`); next(); }); // MCP SSE endpoints for Mastra integration let sseTransport = null; // SSE endpoint for establishing connection app.get('/sse', async (req, res) => { console.log('SSE connection request received'); try { // Create SSE transport with /messages as the POST endpoint sseTransport = new SSEServerTransport('/messages', res); // Handle connection cleanup res.on('close', () => { console.log('SSE connection closed'); sseTransport = null; }); await server.connect(sseTransport); console.log('SSE transport connected successfully'); } catch (error) { console.error('SSE connection error:', error); if (!res.headersSent) { res.status(500).json({ error: 'Failed to establish SSE connection' }); } } }); // POST endpoint for client-to-server messages app.post('/messages', express.json(), async (req, res) => { console.log('POST message received:', req.body); try { if (sseTransport) { // Use the transport's built-in message handler await sseTransport.handlePostMessage(req, res); } else { res.status(503).json({ error: 'SSE transport not initialized' }); } } catch (error) { console.error('Message handling error:', error); if (!res.headersSent) { res.status(500).json({ error: 'Failed to handle message' }); } } }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'tokyo-metro-mcp-server' }); }); // List available tools app.get('/tools', async (req, res) => { try { const response = await server.request({ method: 'tools/list' }, ListToolsRequestSchema); res.json(response); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Execute tool app.post('/tools/:toolName', async (req, res) => { try { const { toolName } = req.params; const args = req.body; const response = await server.request({ method: 'tools/call', params: { name: toolName, arguments: args, }, }, CallToolRequestSchema); res.json(response); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Direct Tokyo Metro API proxy app.post('/tokyo-metro/query', async (req, res) => { try { const { api_url, columns, limit, search_conditions } = req.body; if (!api_url) { return res.status(400).json({ error: 'api_url is required' }); } const response = await server.request({ method: 'tools/call', params: { name: 'tokyo_metro_query', arguments: { api_url, columns, limit, search_conditions }, }, }, CallToolRequestSchema); res.json(response); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Sample COVID data endpoint app.get('/tokyo-metro/covid', async (req, res) => { try { const { start_date, end_date, limit } = req.query; const response = await server.request({ method: 'tools/call', params: { name: 'get_sample_covid_data', arguments: { start_date: start_date, end_date: end_date, limit: limit ? Number(limit) : undefined }, }, }, CallToolRequestSchema); res.json(response); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.listen(PORT, () => { console.log(`Tokyo Metro MCP server running on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`API docs: http://localhost:${PORT}/tools`); }); return app; } async function main() { const mode = process.env.MODE || 'stdio'; if (mode === 'http') { // HTTP mode - don't connect stdio, let SSE endpoint handle connections createHttpServer(); } else { // Default stdio mode for MCP const transport = new StdioServerTransport(); await server.connect(transport); console.error('Tokyo Metro MCP server running on stdio'); } } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); }); //# sourceMappingURL=index.js.map