@agentx-ai/notion-mcp-server
Version:
AgentX MCP server for Notion API with user-friendly environment variables
210 lines (185 loc) • 6.24 kB
text/typescript
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;
}
}