#!/usr/bin/env node
import express, { Request, Response } from 'express';
import cors from 'cors';
import { logger } from './logger.js';
import { FileStorage } from './storage.js';
import { v4 as uuidv4 } from 'uuid';
const app = express();
const storage = new FileStorage();
// CORS設定
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.use(express.json());
// 認証ミドルウェア(オプション)
const authenticate = (req: Request, res: Response, next: Function) => {
const apiKey = process.env.OPENAI_API_KEY;
if (apiKey) {
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
}
next();
};
// ========================================
// Well-known エンドポイント (MCPを提供していないので404)
// ========================================
// このサーバーは MCP (Model Context Protocol) サーバーではなく、
// シンプルなREST APIサーバーです。
// MCPクライアント(ChatGPT等)が探しに来るエンドポイントは
// すべて404を返すことで「MCPは提供していない」ことを示します。
// RFC 9728: OAuth 2.0 Protected Resource Metadata
// MCPを提供していないので404
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.status(404).end();
});
app.get('/.well-known/oauth-protected-resource/sse', (req, res) => {
res.status(404).end();
});
// RFC 8414: OAuth 2.0 Authorization Server Metadata
// MCPを提供していないので404
app.get('/.well-known/oauth-authorization-server', (req, res) => {
res.status(404).end();
});
app.get('/.well-known/oauth-authorization-server/sse', (req, res) => {
res.status(404).end();
});
// OpenID Connect Discovery
// MCPを提供していないので404
app.get('/.well-known/openid-configuration', (req, res) => {
res.status(404).end();
});
app.get('/.well-known/openid-configuration/sse', (req, res) => {
res.status(404).end();
});
app.get('/sse/.well-known/openid-configuration', (req, res) => {
res.status(404).end();
});
// JWKS
app.get('/.well-known/jwks.json', (req, res) => {
res.status(404).end();
});
// SSEエンドポイント (MCPトランスポートではないので404)
app.get('/sse', (req, res) => {
res.status(404).end();
});
app.post('/sse', (req, res) => {
res.status(404).end();
});
// Dynamic Client Registration
app.post('/oauth/register', (req, res) => {
res.status(404).end();
});
// 任意のwell-knownパス
app.all('/.well-known/*', (req, res) => {
res.status(404).end();
});
app.all('*/.well-known/*', (req, res) => {
res.status(404).end();
});
// ========================================
// OpenAPI Schema エンドポイント
// ========================================
app.get('/.well-known/openapi.json', (req, res) => {
const schema = {
openapi: '3.1.0',
info: {
title: 'Universal MCP Server API',
description: 'A simple key-value storage server with file operations. No authentication required.',
version: '1.0.0',
},
servers: [
{
url: process.env.PUBLIC_URL || `https://${req.headers.host}`,
},
],
// 認証スキームは定義しない = 認証不要
paths: {
'/storage/write': {
post: {
operationId: 'writeFile',
summary: 'Write or update a file',
description: 'Store content with a specified key',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['key', 'content'],
properties: {
key: {
type: 'string',
description: 'The key/filename to store the content under',
},
content: {
type: 'string',
description: 'The content to store',
},
},
},
},
},
},
responses: {
'200': {
description: 'File written successfully',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
key: { type: 'string' },
message: { type: 'string' },
},
},
},
},
},
},
},
},
'/storage/read/{key}': {
get: {
operationId: 'readFile',
summary: 'Read a file',
description: 'Retrieve content by key',
parameters: [
{
name: 'key',
in: 'path',
required: true,
schema: {
type: 'string',
},
description: 'The key/filename to read',
},
],
responses: {
'200': {
description: 'File content',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
key: { type: 'string' },
content: { type: 'string' },
},
},
},
},
},
'404': {
description: 'File not found',
},
},
},
},
'/storage/delete/{key}': {
delete: {
operationId: 'deleteFile',
summary: 'Delete a file',
description: 'Remove a file by key',
parameters: [
{
name: 'key',
in: 'path',
required: true,
schema: {
type: 'string',
},
description: 'The key/filename to delete',
},
],
responses: {
'200': {
description: 'File deleted successfully',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
key: { type: 'string' },
message: { type: 'string' },
},
},
},
},
},
},
},
},
'/storage/list': {
get: {
operationId: 'listFiles',
summary: 'List all files',
description: 'Get a list of all stored files',
parameters: [
{
name: 'pattern',
in: 'query',
required: false,
schema: {
type: 'string',
},
description: 'Optional pattern to filter files',
},
],
responses: {
'200': {
description: 'List of files',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
files: {
type: 'array',
items: { type: 'string' },
},
count: { type: 'number' },
},
},
},
},
},
},
},
},
'/storage/search': {
get: {
operationId: 'searchFiles',
summary: 'Search file contents',
description: 'Search for files containing specific text',
parameters: [
{
name: 'query',
in: 'query',
required: true,
schema: {
type: 'string',
},
description: 'Text to search for in file contents',
},
],
responses: {
'200': {
description: 'Search results',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
results: {
type: 'array',
items: {
type: 'object',
properties: {
key: { type: 'string' },
content: { type: 'string' },
},
},
},
count: { type: 'number' },
},
},
},
},
},
},
},
},
},
};
res.json(schema);
});
// ========================================
// Storage API エンドポイント
// ========================================
// Write/Update file
app.post('/storage/write', authenticate, async (req, res) => {
const requestId = uuidv4();
try {
const { key, content } = req.body;
if (!key || content === undefined) {
return res.status(400).json({ error: 'Missing key or content' });
}
logger.info('Write request', { operation: 'write', key, requestId });
await storage.write(key, content, requestId);
res.json({
success: true,
key,
message: 'File written successfully',
});
} catch (error: any) {
logger.error('Write failed', error, { requestId });
res.status(500).json({ error: error.message });
}
});
// Read file
app.get('/storage/read/:key', authenticate, async (req, res) => {
const requestId = uuidv4();
try {
const { key } = req.params;
logger.info('Read request', { operation: 'read', key, requestId });
const content = await storage.read(key, requestId);
if (content === null) {
return res.status(404).json({ error: 'File not found' });
}
res.json({ key, content });
} catch (error: any) {
logger.error('Read failed', error, { requestId });
res.status(500).json({ error: error.message });
}
});
// Delete file
app.delete('/storage/delete/:key', authenticate, async (req, res) => {
const requestId = uuidv4();
try {
const { key } = req.params;
logger.info('Delete request', { operation: 'delete', key, requestId });
await storage.delete(key, requestId);
res.json({
success: true,
key,
message: 'File deleted successfully',
});
} catch (error: any) {
logger.error('Delete failed', error, { requestId });
res.status(500).json({ error: error.message });
}
});
// List files
app.get('/storage/list', authenticate, async (req, res) => {
const requestId = uuidv4();
try {
const pattern = req.query.pattern as string | undefined;
logger.info('List request', { operation: 'list', pattern, requestId });
const files = await storage.list(pattern, requestId);
res.json({ files, count: files.length });
} catch (error: any) {
logger.error('List failed', error, { requestId });
res.status(500).json({ error: error.message });
}
});
// Search files
app.get('/storage/search', authenticate, async (req, res) => {
const requestId = uuidv4();
try {
const query = req.query.query as string;
if (!query) {
return res.status(400).json({ error: 'Missing query parameter' });
}
logger.info('Search request', { operation: 'search', query, requestId });
const results = await storage.search(query, requestId);
res.json({ results, count: results.length });
} catch (error: any) {
logger.error('Search failed', error, { requestId });
res.status(500).json({ error: error.message });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Root endpoint
app.get('/', (req, res) => {
res.json({
name: 'Universal MCP Server - OpenAI Compatible API',
version: '1.0.0',
endpoints: {
schema: '/.well-known/openapi.json',
health: '/health',
storage: {
write: 'POST /storage/write',
read: 'GET /storage/read/:key',
delete: 'DELETE /storage/delete/:key',
list: 'GET /storage/list',
search: 'GET /storage/search?query=...',
},
},
authentication: process.env.OPENAI_API_KEY ? 'API Key (Bearer Token)' : 'None',
});
});
// Start server
const PORT = parseInt(process.env.PORT || '3000', 10);
const HOST = process.env.HOST || '0.0.0.0';
app.listen(PORT, HOST, () => {
logger.info('OpenAI-compatible server started', {
port: PORT,
host: HOST,
schemaUrl: `http://${HOST}:${PORT}/.well-known/openapi.json`,
});
console.log(`\n🚀 OpenAI-compatible API Server running on http://${HOST}:${PORT}`);
console.log(`📋 OpenAPI Schema: http://${HOST}:${PORT}/.well-known/openapi.json`);
console.log(`\nAvailable endpoints:`);
console.log(` POST /storage/write`);
console.log(` GET /storage/read/:key`);
console.log(` DELETE /storage/delete/:key`);
console.log(` GET /storage/list`);
console.log(` GET /storage/search?query=...`);
console.log(` GET /health`);
console.log(`\nSet OPENAI_API_KEY environment variable to enable authentication.\n`);
});