#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { logger } from './logger.js';
import { FileStorage } from './storage.js';
import { HttpMCPServer } from './http-server.js';
import { v4 as uuidv4 } from 'uuid';
// 環境変数から設定を読み込み
const MODE = process.env.MCP_MODE || 'stdio'; // 'stdio' or 'http'
const HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT || '3000');
const HTTP_HOST = process.env.MCP_HTTP_HOST || '0.0.0.0';
const API_KEY = process.env.MCP_API_KEY; // オプション:認証用APIキー
// ストレージインスタンス
const storage = new FileStorage();
// MCPサーバー作成
function createMCPServer(): Server {
const server = new Server(
{
name: 'universal-mcp-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
logger.info('MCP Server initializing', {
mode: MODE,
sessionId: logger.getSessionId(),
capabilities: ['resources', 'tools']
});
// ========================================
// Resources(読み込み)
// ========================================
// リソース一覧
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'file:///{key}',
name: 'File Resource',
description: 'Read a file by key',
mimeType: 'text/plain',
},
{
uri: 'file:///list',
name: 'File List',
description: 'List all available files',
mimeType: 'application/json',
},
],
};
});
// リソース読み込み
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const requestId = uuidv4();
const uri = request.params.uri;
try {
logger.info('Resource request received', {
operation: 'resource:read',
uri,
requestId
});
if (uri === 'file:///list') {
// ファイル一覧
const files = await storage.list(undefined, requestId);
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify({ files }, null, 2)
}]
};
} else if (uri.startsWith('file:///')) {
// 個別ファイル読み込み
const key = uri.slice(8); // Remove 'file:///'
const content = await storage.read(key, requestId);
if (content === null) {
throw new Error(`File not found: ${key}`);
}
return {
contents: [{
uri,
mimeType: 'text/plain',
text: content
}]
};
} else {
throw new Error(`Unknown resource URI: ${uri}`);
}
} catch (error) {
logger.error('Resource request failed', error as Error, {
operation: 'resource:read',
uri,
requestId
});
throw error;
}
});
// ========================================
// Tools(書き込み・実行・検索)
// ========================================
// ツール一覧
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'write_file',
description: 'Write content to a file',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'The file key/name',
},
content: {
type: 'string',
description: 'The content to write',
},
},
required: ['key', 'content'],
},
},
{
name: 'delete_file',
description: 'Delete a file',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'The file key/name to delete',
},
},
required: ['key'],
},
},
{
name: 'search_files',
description: 'Search for files containing specific text',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The text to search for',
},
},
required: ['query'],
},
},
],
};
});
// ツール実行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const requestId = uuidv4();
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'write_file': {
const { key, content } = args as { key: string; content: string };
logger.info('Tool request received', {
operation: 'tool:write',
toolName: 'write_file',
key,
requestId
});
await storage.write(key, content, requestId);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `File '${key}' written successfully`,
key
}, null, 2)
}]
};
}
case 'delete_file': {
const { key } = args as { key: string };
logger.info('Tool request received', {
operation: 'tool:delete',
toolName: 'delete_file',
key,
requestId
});
const deleted = await storage.delete(key, requestId);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: deleted,
message: deleted ? `File '${key}' deleted successfully` : `File '${key}' not found`,
key
}, null, 2)
}]
};
}
case 'search_files': {
const { query } = args as { query: string };
logger.info('Tool request received', {
operation: 'tool:search',
toolName: 'search_files',
query,
requestId
});
const results = await storage.search(query, requestId);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
query,
resultsCount: results.length,
results: results.map(r => ({ key: r.key, preview: r.content.slice(0, 100) + '...' }))
}, null, 2)
}]
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
logger.error('Tool request failed', error as Error, {
operation: `tool:${name}`,
toolName: name,
requestId
});
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: (error as Error).message
}, null, 2)
}],
isError: true
};
}
});
return server;
}
// ========================================
// サーバー起動
// ========================================
async function main() {
try {
const server = createMCPServer();
if (MODE === 'http') {
// HTTPモード(OpenAI、ngrok対応)
const httpServer = new HttpMCPServer(server, {
port: HTTP_PORT,
host: HTTP_HOST,
apiKey: API_KEY,
enableCors: true
});
await httpServer.start();
logger.info('MCP Server ready in HTTP mode', {
mode: 'http',
port: HTTP_PORT,
host: HTTP_HOST,
authentication: !!API_KEY,
sessionId: logger.getSessionId(),
status: 'ready'
});
if (API_KEY) {
logger.info('Authentication enabled', {
hint: 'Use Authorization: Bearer <API_KEY> header'
});
} else {
logger.warn('Authentication disabled', {
warning: 'Server is running without authentication. Set MCP_API_KEY environment variable for security.'
});
}
} else {
// STDIOモード(Claude Desktop)
const transport = new StdioServerTransport();
logger.info('Connecting to transport', {
mode: 'stdio',
transport: 'stdio',
sessionId: logger.getSessionId()
});
await server.connect(transport);
logger.info('MCP Server connected and ready', {
mode: 'stdio',
sessionId: logger.getSessionId(),
status: 'ready'
});
}
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('Shutdown signal received', { signal: 'SIGINT' });
await server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('Shutdown signal received', { signal: 'SIGTERM' });
await server.close();
process.exit(0);
});
} catch (error) {
logger.error('Failed to start MCP server', error as Error);
process.exit(1);
}
}
main();