tokyo-metro-mcp-server
Version:
MCP server for Tokyo Metro API integration
397 lines • 15.9 kB
JavaScript
#!/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