UNPKG

@agentx-ai/notion-mcp-server

Version:

AgentX MCP server for Notion API with user-friendly environment variables

210 lines (185 loc) 6.24 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, JSONRPCResponse, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { JSONSchema7 as IJsonSchema } from "json-schema"; import { OpenAPIToMCPConverter } from "../openapi/parser"; import { HttpClient, HttpClientError } from "../client/http-client"; import { OpenAPIV3 } from "openapi-types"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; type PathItemObject = OpenAPIV3.PathItemObject & { get?: OpenAPIV3.OperationObject; put?: OpenAPIV3.OperationObject; post?: OpenAPIV3.OperationObject; delete?: OpenAPIV3.OperationObject; patch?: OpenAPIV3.OperationObject; }; type NewToolDefinition = { methods: Array<{ name: string; description: string; inputSchema: IJsonSchema & { type: "object" }; returnSchema?: IJsonSchema; }>; }; // import this class, extend and return server export class MCPProxy { private server: Server; private httpClient: HttpClient; private tools: Record<string, NewToolDefinition>; private openApiLookup: Record< string, OpenAPIV3.OperationObject & { method: string; path: string } >; constructor(name: string, openApiSpec: OpenAPIV3.Document) { this.server = new Server( { name, version: "1.0.0" }, { capabilities: { tools: {} } } ); const baseUrl = openApiSpec.servers?.[0].url; if (!baseUrl) { throw new Error("No base URL found in OpenAPI spec"); } this.httpClient = new HttpClient( { baseUrl, headers: this.parseHeadersFromEnv(), }, openApiSpec ); // Convert OpenAPI spec to MCP tools const converter = new OpenAPIToMCPConverter(openApiSpec); const { tools, openApiLookup } = converter.convertToMCPTools(); this.tools = tools; this.openApiLookup = openApiLookup; this.setupHandlers(); } private setupHandlers() { // Handle tool listing this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = []; // Add methods as separate tools to match the MCP format Object.entries(this.tools).forEach(([toolName, def]) => { def.methods.forEach((method) => { const toolNameWithMethod = `${toolName}-${method.name}`; const truncatedToolName = this.truncateToolName(toolNameWithMethod); tools.push({ name: truncatedToolName, description: method.description, inputSchema: method.inputSchema as Tool["inputSchema"], }); }); }); return { tools }; }); // Handle tool calling this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: params } = request.params; // Find the operation in OpenAPI spec const operation = this.findOperation(name); if (!operation) { throw new Error(`Method ${name} not found`); } try { // Execute the operation const response = await this.httpClient.executeOperation( operation, params ); // Convert response to MCP format return { content: [ { type: "text", // currently this is the only type that seems to be used by mcp server text: JSON.stringify(response.data), // TODO: pass through the http status code text? }, ], }; } catch (error) { console.error("Error in tool call", error); if (error instanceof HttpClientError) { console.error( "HttpClientError encountered, returning structured error", error ); const data = error.data?.response?.data ?? error.data ?? {}; return { content: [ { type: "text", text: JSON.stringify({ status: "error", // TODO: get this from http status code? ...(typeof data === "object" ? data : { data: data }), }), }, ], }; } throw error; } }); } private findOperation( operationId: string ): (OpenAPIV3.OperationObject & { method: string; path: string }) | null { return this.openApiLookup[operationId] ?? null; } private parseHeadersFromEnv(): Record<string, string> { const headers: Record<string, string> = {}; // Get Notion API token const apiToken = process.env.NOTION_API_TOKEN; if (apiToken) { headers["Authorization"] = `Bearer ${apiToken}`; } else { console.warn( "NOTION_API_TOKEN environment variable is not set. API calls may fail." ); } // Get Notion version (optional, defaults to 2022-06-28) const version = process.env.NOTION_VERSION || "2022-06-28"; headers["Notion-Version"] = version; // Fallback to legacy OPENAPI_MCP_HEADERS for backward compatibility const headersJson = process.env.OPENAPI_MCP_HEADERS; if (headersJson) { try { const legacyHeaders = JSON.parse(headersJson); if (typeof legacyHeaders === "object" && legacyHeaders !== null) { // Merge legacy headers, but new headers take precedence Object.assign(headers, legacyHeaders); } } catch (error) { console.warn( "Failed to parse OPENAPI_MCP_HEADERS environment variable:", error ); } } return headers; } private getContentType(headers: Headers): "text" | "image" | "binary" { const contentType = headers.get("content-type"); if (!contentType) return "binary"; if (contentType.includes("text") || contentType.includes("json")) { return "text"; } else if (contentType.includes("image")) { return "image"; } return "binary"; } private truncateToolName(name: string): string { if (name.length <= 64) { return name; } return name.slice(0, 64); } async connect(transport: Transport) { // The SDK will handle stdio communication await this.server.connect(transport); } getServer() { return this.server; } }