@cyanheads/pubchem-mcp-server 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +236 -0
- package/dist/config/index.d.ts +98 -0
- package/dist/config/index.js +326 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +214 -0
- package/dist/mcp-server/server.d.ts +21 -0
- package/dist/mcp-server/server.js +124 -0
- package/dist/mcp-server/tools/fetchAssaySummary/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchAssaySummary/index.js +7 -0
- package/dist/mcp-server/tools/fetchAssaySummary/logic.d.ts +71 -0
- package/dist/mcp-server/tools/fetchAssaySummary/logic.js +96 -0
- package/dist/mcp-server/tools/fetchAssaySummary/registration.d.ts +12 -0
- package/dist/mcp-server/tools/fetchAssaySummary/registration.js +68 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/index.js +7 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/logic.d.ts +131 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/logic.js +100 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/registration.js +52 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/index.js +7 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/logic.d.ts +89 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/logic.js +145 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/registration.js +52 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/index.js +7 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/logic.d.ts +69 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/logic.js +97 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/registration.js +79 -0
- package/dist/mcp-server/tools/getCompoundImage/index.d.ts +7 -0
- package/dist/mcp-server/tools/getCompoundImage/index.js +7 -0
- package/dist/mcp-server/tools/getCompoundImage/logic.d.ts +42 -0
- package/dist/mcp-server/tools/getCompoundImage/logic.js +68 -0
- package/dist/mcp-server/tools/getCompoundImage/registration.d.ts +12 -0
- package/dist/mcp-server/tools/getCompoundImage/registration.js +57 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/index.js +7 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/logic.d.ts +35 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/logic.js +64 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/logic.d.ts +35 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/logic.js +59 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/registration.js +51 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/logic.d.ts +38 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/logic.js +75 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/logic.d.ts +41 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/logic.js +82 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/logic.d.ts +41 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/logic.js +78 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/registration.d.ts +13 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/registration.js +63 -0
- package/dist/mcp-server/transports/auth/core/authContext.d.ts +33 -0
- package/dist/mcp-server/transports/auth/core/authContext.js +24 -0
- package/dist/mcp-server/transports/auth/core/authTypes.d.ts +17 -0
- package/dist/mcp-server/transports/auth/core/authTypes.js +5 -0
- package/dist/mcp-server/transports/auth/core/authUtils.d.ts +18 -0
- package/dist/mcp-server/transports/auth/core/authUtils.js +45 -0
- package/dist/mcp-server/transports/auth/index.d.ts +10 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +27 -0
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +149 -0
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +20 -0
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +124 -0
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +21 -0
- package/dist/mcp-server/transports/httpTransport.js +208 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
- package/dist/mcp-server/transports/stdioTransport.js +63 -0
- package/dist/services/pubchem/pubchemApiClient.d.ts +52 -0
- package/dist/services/pubchem/pubchemApiClient.js +120 -0
- package/dist/types-global/errors.d.ts +127 -0
- package/dist/types-global/errors.js +126 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.js +20 -0
- package/dist/utils/internal/errorHandler.d.ts +176 -0
- package/dist/utils/internal/errorHandler.js +335 -0
- package/dist/utils/internal/index.d.ts +9 -0
- package/dist/utils/internal/index.js +9 -0
- package/dist/utils/internal/logger.d.ts +148 -0
- package/dist/utils/internal/logger.js +430 -0
- package/dist/utils/internal/requestContext.d.ts +83 -0
- package/dist/utils/internal/requestContext.js +72 -0
- package/dist/utils/metrics/index.d.ts +7 -0
- package/dist/utils/metrics/index.js +7 -0
- package/dist/utils/metrics/tokenCounter.d.ts +35 -0
- package/dist/utils/metrics/tokenCounter.js +110 -0
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/parsing/dateParser.d.ts +73 -0
- package/dist/utils/parsing/dateParser.js +107 -0
- package/dist/utils/parsing/index.d.ts +8 -0
- package/dist/utils/parsing/index.js +8 -0
- package/dist/utils/parsing/jsonParser.d.ts +82 -0
- package/dist/utils/parsing/jsonParser.js +126 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +152 -0
- package/dist/utils/security/idGenerator.d.ts +137 -0
- package/dist/utils/security/idGenerator.js +215 -0
- package/dist/utils/security/index.d.ts +9 -0
- package/dist/utils/security/index.js +9 -0
- package/dist/utils/security/rateLimiter.d.ts +108 -0
- package/dist/utils/security/rateLimiter.js +179 -0
- package/dist/utils/security/sanitization.d.ts +184 -0
- package/dist/utils/security/sanitization.js +453 -0
- package/package.json +108 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configures and starts the Streamable HTTP MCP transport using Hono.
|
|
3
|
+
* This module integrates the `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`
|
|
4
|
+
* into a Hono web server. Its responsibilities include:
|
|
5
|
+
* - Creating a Hono server instance.
|
|
6
|
+
* - Applying and configuring middleware for CORS, rate limiting, and authentication (JWT/OAuth).
|
|
7
|
+
* - Defining the routes (`/mcp` endpoint for POST, GET, DELETE) to handle the MCP lifecycle.
|
|
8
|
+
* - Orchestrating session management by mapping session IDs to SDK transport instances.
|
|
9
|
+
* - Implementing port-binding logic with automatic retry on conflicts.
|
|
10
|
+
*
|
|
11
|
+
* The underlying implementation of the MCP Streamable HTTP specification, including
|
|
12
|
+
* Server-Sent Events (SSE) for streaming, is handled by the SDK's transport class.
|
|
13
|
+
*
|
|
14
|
+
* Specification Reference:
|
|
15
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
16
|
+
* @module src/mcp-server/transports/httpTransport
|
|
17
|
+
*/
|
|
18
|
+
import { serve } from "@hono/node-server";
|
|
19
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import { Hono } from "hono";
|
|
22
|
+
import { cors } from "hono/cors";
|
|
23
|
+
import http from "http";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { config } from "../../config/index.js";
|
|
26
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
27
|
+
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
28
|
+
import { jwtAuthMiddleware, oauthMiddleware, } from "./auth/index.js";
|
|
29
|
+
import { httpErrorHandler } from "./httpErrorHandler.js";
|
|
30
|
+
const HTTP_PORT = config.mcpHttpPort;
|
|
31
|
+
const HTTP_HOST = config.mcpHttpHost;
|
|
32
|
+
const MCP_ENDPOINT_PATH = "/mcp";
|
|
33
|
+
const MAX_PORT_RETRIES = 15;
|
|
34
|
+
// The transports map will store active sessions, keyed by session ID.
|
|
35
|
+
// NOTE: This is an in-memory session store, which is a known limitation for scalability.
|
|
36
|
+
// It will not work in a multi-process (clustered) or serverless environment.
|
|
37
|
+
// For a scalable deployment, this would need to be replaced with a distributed
|
|
38
|
+
// store like Redis or Memcached.
|
|
39
|
+
const transports = {};
|
|
40
|
+
async function isPortInUse(port, host, parentContext) {
|
|
41
|
+
const checkContext = requestContextService.createRequestContext({
|
|
42
|
+
...parentContext,
|
|
43
|
+
operation: "isPortInUse",
|
|
44
|
+
port,
|
|
45
|
+
host,
|
|
46
|
+
});
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const tempServer = http.createServer();
|
|
49
|
+
tempServer
|
|
50
|
+
.once("error", (err) => {
|
|
51
|
+
resolve(err.code === "EADDRINUSE");
|
|
52
|
+
})
|
|
53
|
+
.once("listening", () => {
|
|
54
|
+
tempServer.close(() => resolve(false));
|
|
55
|
+
})
|
|
56
|
+
.listen(port, host);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
60
|
+
const startContext = requestContextService.createRequestContext({
|
|
61
|
+
...parentContext,
|
|
62
|
+
operation: "startHttpServerWithRetry",
|
|
63
|
+
});
|
|
64
|
+
return new Promise(async (resolve, reject) => {
|
|
65
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
66
|
+
const currentPort = initialPort + i;
|
|
67
|
+
const attemptContext = {
|
|
68
|
+
...startContext,
|
|
69
|
+
port: currentPort,
|
|
70
|
+
attempt: i + 1,
|
|
71
|
+
};
|
|
72
|
+
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
73
|
+
logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
|
|
78
|
+
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
79
|
+
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
80
|
+
...attemptContext,
|
|
81
|
+
address: serverAddress,
|
|
82
|
+
});
|
|
83
|
+
if (process.stdout.isTTY) {
|
|
84
|
+
console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
resolve(serverInstance);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err.code !== "EADDRINUSE") {
|
|
92
|
+
reject(err);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
reject(new Error("Failed to bind to any port after multiple retries."));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
101
|
+
const app = new Hono();
|
|
102
|
+
const transportContext = requestContextService.createRequestContext({
|
|
103
|
+
...parentContext,
|
|
104
|
+
component: "HttpTransportSetup",
|
|
105
|
+
});
|
|
106
|
+
app.use("*", cors({
|
|
107
|
+
origin: config.mcpAllowedOrigins || [],
|
|
108
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
109
|
+
allowHeaders: [
|
|
110
|
+
"Content-Type",
|
|
111
|
+
"Mcp-Session-Id",
|
|
112
|
+
"Last-Event-ID",
|
|
113
|
+
"Authorization",
|
|
114
|
+
],
|
|
115
|
+
credentials: true,
|
|
116
|
+
}));
|
|
117
|
+
app.use("*", async (c, next) => {
|
|
118
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
119
|
+
await next();
|
|
120
|
+
});
|
|
121
|
+
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
122
|
+
// NOTE (Security): The 'x-forwarded-for' header is used for rate limiting.
|
|
123
|
+
// This is only secure if the server is run behind a trusted proxy that
|
|
124
|
+
// correctly sets or validates this header.
|
|
125
|
+
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
126
|
+
const context = requestContextService.createRequestContext({
|
|
127
|
+
operation: "httpRateLimitCheck",
|
|
128
|
+
ipAddress: clientIp,
|
|
129
|
+
});
|
|
130
|
+
// Let the centralized error handler catch rate limit errors
|
|
131
|
+
rateLimiter.check(clientIp, context);
|
|
132
|
+
await next();
|
|
133
|
+
});
|
|
134
|
+
if (config.mcpAuthMode === "oauth") {
|
|
135
|
+
app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
app.use(MCP_ENDPOINT_PATH, jwtAuthMiddleware);
|
|
139
|
+
}
|
|
140
|
+
// Centralized Error Handling
|
|
141
|
+
app.onError(httpErrorHandler);
|
|
142
|
+
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
143
|
+
const postContext = requestContextService.createRequestContext({
|
|
144
|
+
...transportContext,
|
|
145
|
+
operation: "handlePost",
|
|
146
|
+
});
|
|
147
|
+
const body = await c.req.json();
|
|
148
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
149
|
+
let transport = sessionId
|
|
150
|
+
? transports[sessionId]
|
|
151
|
+
: undefined;
|
|
152
|
+
if (isInitializeRequest(body)) {
|
|
153
|
+
// If a transport already exists for a session, it's a re-initialization.
|
|
154
|
+
if (transport) {
|
|
155
|
+
logger.warning("Re-initializing existing session.", {
|
|
156
|
+
...postContext,
|
|
157
|
+
sessionId,
|
|
158
|
+
});
|
|
159
|
+
await transport.close(); // This will trigger the onclose handler.
|
|
160
|
+
}
|
|
161
|
+
// Create a new transport for a new session.
|
|
162
|
+
const newTransport = new StreamableHTTPServerTransport({
|
|
163
|
+
sessionIdGenerator: () => randomUUID(),
|
|
164
|
+
onsessioninitialized: (newId) => {
|
|
165
|
+
transports[newId] = newTransport;
|
|
166
|
+
logger.info(`HTTP Session created: ${newId}`, {
|
|
167
|
+
...postContext,
|
|
168
|
+
newSessionId: newId,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
// Set up cleanup logic for when the transport is closed.
|
|
173
|
+
newTransport.onclose = () => {
|
|
174
|
+
const closedSessionId = newTransport.sessionId;
|
|
175
|
+
if (closedSessionId && transports[closedSessionId]) {
|
|
176
|
+
delete transports[closedSessionId];
|
|
177
|
+
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
178
|
+
...postContext,
|
|
179
|
+
closedSessionId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Connect the new transport to a new server instance.
|
|
184
|
+
const server = await createServerInstanceFn();
|
|
185
|
+
await server.connect(newTransport);
|
|
186
|
+
transport = newTransport;
|
|
187
|
+
}
|
|
188
|
+
else if (!transport) {
|
|
189
|
+
// If it's not an initialization request and no transport was found, it's an error.
|
|
190
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
|
|
191
|
+
}
|
|
192
|
+
// Pass the request to the transport to handle.
|
|
193
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
194
|
+
});
|
|
195
|
+
// A reusable handler for GET and DELETE requests which operate on existing sessions.
|
|
196
|
+
const handleSessionRequest = async (c) => {
|
|
197
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
198
|
+
const transport = sessionId ? transports[sessionId] : undefined;
|
|
199
|
+
if (!transport) {
|
|
200
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
|
|
201
|
+
}
|
|
202
|
+
// Let the transport handle the streaming (GET) or termination (DELETE) request.
|
|
203
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
204
|
+
};
|
|
205
|
+
app.get(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
206
|
+
app.delete(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
207
|
+
return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
|
|
208
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles the setup and connection for the Stdio MCP transport.
|
|
3
|
+
* Implements the MCP Specification 2025-03-26 for stdio transport.
|
|
4
|
+
* This transport communicates directly over standard input (stdin) and
|
|
5
|
+
* standard output (stdout), typically used when the MCP server is launched
|
|
6
|
+
* as a child process by a host application.
|
|
7
|
+
*
|
|
8
|
+
* Specification Reference:
|
|
9
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
|
|
10
|
+
*
|
|
11
|
+
* --- Authentication Note ---
|
|
12
|
+
* As per the MCP Authorization Specification (2025-03-26, Section 1.2),
|
|
13
|
+
* STDIO transports SHOULD NOT implement HTTP-based authentication flows.
|
|
14
|
+
* Authorization is typically handled implicitly by the host application
|
|
15
|
+
* controlling the server process. This implementation follows that guideline.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
18
|
+
* @module src/mcp-server/transports/stdioTransport
|
|
19
|
+
*/
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
import { RequestContext } from "../../utils/index.js";
|
|
22
|
+
/**
|
|
23
|
+
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
|
+
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
25
|
+
* communication over `process.stdin` and `process.stdout` according to the
|
|
26
|
+
* MCP stdio transport specification.
|
|
27
|
+
*
|
|
28
|
+
* MCP Spec Points Covered by SDK's `StdioServerTransport`:
|
|
29
|
+
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
30
|
+
* - Writes JSON-RPC messages to stdout.
|
|
31
|
+
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
32
|
+
* - Ensures only valid MCP messages are written to stdout.
|
|
33
|
+
*
|
|
34
|
+
* Logging via the `logger` utility MAY result in output to stderr, which is
|
|
35
|
+
* permitted by the spec for logging purposes.
|
|
36
|
+
*
|
|
37
|
+
* @param server - The `McpServer` instance.
|
|
38
|
+
* @param parentContext - The logging and tracing context from the calling function.
|
|
39
|
+
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
|
+
* @throws {Error} If the connection fails during setup.
|
|
41
|
+
*/
|
|
42
|
+
export declare function connectStdioTransport(server: McpServer, parentContext: RequestContext): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles the setup and connection for the Stdio MCP transport.
|
|
3
|
+
* Implements the MCP Specification 2025-03-26 for stdio transport.
|
|
4
|
+
* This transport communicates directly over standard input (stdin) and
|
|
5
|
+
* standard output (stdout), typically used when the MCP server is launched
|
|
6
|
+
* as a child process by a host application.
|
|
7
|
+
*
|
|
8
|
+
* Specification Reference:
|
|
9
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
|
|
10
|
+
*
|
|
11
|
+
* --- Authentication Note ---
|
|
12
|
+
* As per the MCP Authorization Specification (2025-03-26, Section 1.2),
|
|
13
|
+
* STDIO transports SHOULD NOT implement HTTP-based authentication flows.
|
|
14
|
+
* Authorization is typically handled implicitly by the host application
|
|
15
|
+
* controlling the server process. This implementation follows that guideline.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
18
|
+
* @module src/mcp-server/transports/stdioTransport
|
|
19
|
+
*/
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { ErrorHandler, logger } from "../../utils/index.js";
|
|
22
|
+
/**
|
|
23
|
+
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
|
+
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
25
|
+
* communication over `process.stdin` and `process.stdout` according to the
|
|
26
|
+
* MCP stdio transport specification.
|
|
27
|
+
*
|
|
28
|
+
* MCP Spec Points Covered by SDK's `StdioServerTransport`:
|
|
29
|
+
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
30
|
+
* - Writes JSON-RPC messages to stdout.
|
|
31
|
+
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
32
|
+
* - Ensures only valid MCP messages are written to stdout.
|
|
33
|
+
*
|
|
34
|
+
* Logging via the `logger` utility MAY result in output to stderr, which is
|
|
35
|
+
* permitted by the spec for logging purposes.
|
|
36
|
+
*
|
|
37
|
+
* @param server - The `McpServer` instance.
|
|
38
|
+
* @param parentContext - The logging and tracing context from the calling function.
|
|
39
|
+
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
|
+
* @throws {Error} If the connection fails during setup.
|
|
41
|
+
*/
|
|
42
|
+
export async function connectStdioTransport(server, parentContext) {
|
|
43
|
+
const operationContext = {
|
|
44
|
+
...parentContext,
|
|
45
|
+
operation: "connectStdioTransport",
|
|
46
|
+
transportType: "Stdio",
|
|
47
|
+
};
|
|
48
|
+
logger.debug("Attempting to connect stdio transport...", operationContext);
|
|
49
|
+
try {
|
|
50
|
+
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
51
|
+
const transport = new StdioServerTransport();
|
|
52
|
+
logger.debug("Connecting McpServer instance to StdioServerTransport...", operationContext);
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
logger.info("MCP Server connected and listening via stdio transport.", operationContext);
|
|
55
|
+
if (process.stdout.isTTY) {
|
|
56
|
+
console.log(`\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
ErrorHandler.handleError(err, { ...operationContext, critical: true });
|
|
61
|
+
throw err; // Re-throw after handling to allow caller to react if necessary
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A singleton service to interact with the PubChem PUG REST API.
|
|
3
|
+
* This client handles URL construction, rate limiting, request execution,
|
|
4
|
+
* and error handling for all PubChem API interactions.
|
|
5
|
+
* @module src/services/pubchem/pubchemApiClient
|
|
6
|
+
*/
|
|
7
|
+
import { type RequestContext } from "../../utils/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* A singleton class to manage all interactions with the PubChem PUG REST API.
|
|
10
|
+
*/
|
|
11
|
+
declare class PubChemApiClient {
|
|
12
|
+
private static instance;
|
|
13
|
+
private readonly limiter;
|
|
14
|
+
/**
|
|
15
|
+
* Private constructor to enforce the singleton pattern.
|
|
16
|
+
* Initializes a rate limiter compliant with PubChem's policy (5 requests/sec).
|
|
17
|
+
*/
|
|
18
|
+
private constructor();
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the singleton instance of the PubChemApiClient.
|
|
21
|
+
* @returns The singleton instance.
|
|
22
|
+
*/
|
|
23
|
+
static getInstance(): PubChemApiClient;
|
|
24
|
+
/**
|
|
25
|
+
* Executes a request to the PubChem API for a JSON response, respecting the rate limit.
|
|
26
|
+
* @param path - The API endpoint path (e.g., '/compound/cid/2244/property/MolecularFormula/JSON').
|
|
27
|
+
* @param context - The request context for tracing and logging.
|
|
28
|
+
* @returns A promise that resolves with the JSON response from the API.
|
|
29
|
+
* @throws {McpError} If the request fails, times out, or the API returns an error.
|
|
30
|
+
*/
|
|
31
|
+
get<T>(path: string, context: RequestContext): Promise<T | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Executes a request to the PubChem API for a binary blob response, respecting the rate limit.
|
|
34
|
+
* @param path - The API endpoint path (e.g., '/compound/cid/2244/PNG').
|
|
35
|
+
* @param context - The request context for tracing and logging.
|
|
36
|
+
* @returns A promise that resolves with an ArrayBuffer of the response body.
|
|
37
|
+
* @throws {McpError} If the request fails or times out.
|
|
38
|
+
*/
|
|
39
|
+
getBlob(path: string, context: RequestContext): Promise<ArrayBuffer>;
|
|
40
|
+
/**
|
|
41
|
+
* Private helper to execute a generic fetch request, handling rate limiting and basic error checking.
|
|
42
|
+
* @param path - The API endpoint path.
|
|
43
|
+
* @param context - The request context.
|
|
44
|
+
* @returns A promise that resolves with the raw Response object.
|
|
45
|
+
*/
|
|
46
|
+
private executeRequest;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Singleton instance of the PubChem API client.
|
|
50
|
+
*/
|
|
51
|
+
export declare const pubChemApiClient: PubChemApiClient;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A singleton service to interact with the PubChem PUG REST API.
|
|
3
|
+
* This client handles URL construction, rate limiting, request execution,
|
|
4
|
+
* and error handling for all PubChem API interactions.
|
|
5
|
+
* @module src/services/pubchem/pubchemApiClient
|
|
6
|
+
*/
|
|
7
|
+
import { RateLimiter } from "limiter";
|
|
8
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
9
|
+
import { fetchWithTimeout, logger, } from "../../utils/index.js";
|
|
10
|
+
const PUBCHEM_API_BASE_URL = "https://pubchem.ncbi.nlm.nih.gov/rest/pug";
|
|
11
|
+
const PUBCHEM_API_TIMEOUT_MS = 30000; // 30 seconds
|
|
12
|
+
/**
|
|
13
|
+
* A singleton class to manage all interactions with the PubChem PUG REST API.
|
|
14
|
+
*/
|
|
15
|
+
class PubChemApiClient {
|
|
16
|
+
/**
|
|
17
|
+
* Private constructor to enforce the singleton pattern.
|
|
18
|
+
* Initializes a rate limiter compliant with PubChem's policy (5 requests/sec).
|
|
19
|
+
*/
|
|
20
|
+
constructor() {
|
|
21
|
+
this.limiter = new RateLimiter({
|
|
22
|
+
tokensPerInterval: 5,
|
|
23
|
+
interval: "second",
|
|
24
|
+
});
|
|
25
|
+
logger.info("PubChemApiClient initialized with a rate limit of 5 requests/second.");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Retrieves the singleton instance of the PubChemApiClient.
|
|
29
|
+
* @returns The singleton instance.
|
|
30
|
+
*/
|
|
31
|
+
static getInstance() {
|
|
32
|
+
if (!PubChemApiClient.instance) {
|
|
33
|
+
PubChemApiClient.instance = new PubChemApiClient();
|
|
34
|
+
}
|
|
35
|
+
return PubChemApiClient.instance;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Executes a request to the PubChem API for a JSON response, respecting the rate limit.
|
|
39
|
+
* @param path - The API endpoint path (e.g., '/compound/cid/2244/property/MolecularFormula/JSON').
|
|
40
|
+
* @param context - The request context for tracing and logging.
|
|
41
|
+
* @returns A promise that resolves with the JSON response from the API.
|
|
42
|
+
* @throws {McpError} If the request fails, times out, or the API returns an error.
|
|
43
|
+
*/
|
|
44
|
+
async get(path, context) {
|
|
45
|
+
const response = await this.executeRequest(path, context);
|
|
46
|
+
// Handle cases where the response is OK but there's no content
|
|
47
|
+
if (response.status === 204) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return response.json();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Executes a request to the PubChem API for a binary blob response, respecting the rate limit.
|
|
54
|
+
* @param path - The API endpoint path (e.g., '/compound/cid/2244/PNG').
|
|
55
|
+
* @param context - The request context for tracing and logging.
|
|
56
|
+
* @returns A promise that resolves with an ArrayBuffer of the response body.
|
|
57
|
+
* @throws {McpError} If the request fails or times out.
|
|
58
|
+
*/
|
|
59
|
+
async getBlob(path, context) {
|
|
60
|
+
const response = await this.executeRequest(path, context);
|
|
61
|
+
return response.arrayBuffer();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Private helper to execute a generic fetch request, handling rate limiting and basic error checking.
|
|
65
|
+
* @param path - The API endpoint path.
|
|
66
|
+
* @param context - The request context.
|
|
67
|
+
* @returns A promise that resolves with the raw Response object.
|
|
68
|
+
*/
|
|
69
|
+
async executeRequest(path, context) {
|
|
70
|
+
await this.limiter.removeTokens(1);
|
|
71
|
+
const url = path.startsWith("http")
|
|
72
|
+
? path
|
|
73
|
+
: `${PUBCHEM_API_BASE_URL}${path}`;
|
|
74
|
+
logger.debug(`Executing PubChem API GET request`, { ...context, url });
|
|
75
|
+
const response = await fetchWithTimeout(url, PUBCHEM_API_TIMEOUT_MS, context);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
const pubChemStatus = response.headers.get("pubchem-pug-status-message") ||
|
|
79
|
+
"No status message";
|
|
80
|
+
logger.error("PubChem API request failed", {
|
|
81
|
+
...context,
|
|
82
|
+
url,
|
|
83
|
+
status: response.status,
|
|
84
|
+
statusText: response.statusText,
|
|
85
|
+
pubChemStatus,
|
|
86
|
+
responseBody: errorText,
|
|
87
|
+
});
|
|
88
|
+
// Map HTTP status codes to McpError codes
|
|
89
|
+
let errorCode;
|
|
90
|
+
switch (response.status) {
|
|
91
|
+
case 400:
|
|
92
|
+
errorCode = BaseErrorCode.INVALID_INPUT;
|
|
93
|
+
break;
|
|
94
|
+
case 404:
|
|
95
|
+
errorCode = BaseErrorCode.NOT_FOUND;
|
|
96
|
+
break;
|
|
97
|
+
case 405:
|
|
98
|
+
errorCode = BaseErrorCode.METHOD_NOT_ALLOWED;
|
|
99
|
+
break;
|
|
100
|
+
case 503:
|
|
101
|
+
errorCode = BaseErrorCode.SERVICE_UNAVAILABLE;
|
|
102
|
+
break;
|
|
103
|
+
case 504:
|
|
104
|
+
errorCode = BaseErrorCode.GATEWAY_TIMEOUT;
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
errorCode = BaseErrorCode.EXTERNAL_SERVICE_ERROR;
|
|
108
|
+
}
|
|
109
|
+
throw new McpError(errorCode, `PubChem API Error: ${pubChemStatus}`, {
|
|
110
|
+
httpStatusCode: response.status,
|
|
111
|
+
details: errorText,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Singleton instance of the PubChem API client.
|
|
119
|
+
*/
|
|
120
|
+
export const pubChemApiClient = PubChemApiClient.getInstance();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines standardized error codes, a custom error class, and related schemas
|
|
3
|
+
* for handling errors within the Model Context Protocol (MCP) server and its components.
|
|
4
|
+
* This module provides a structured way to represent and communicate errors, ensuring
|
|
5
|
+
* consistency and clarity for both server-side operations and client-side error handling.
|
|
6
|
+
* @module src/types-global/errors
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
/**
|
|
10
|
+
* Defines a comprehensive set of standardized error codes for common issues encountered
|
|
11
|
+
* within MCP servers, tools, or related operations. These codes are designed to help
|
|
12
|
+
* clients and developers programmatically understand the nature of an error, facilitating
|
|
13
|
+
* more precise error handling and debugging.
|
|
14
|
+
*/
|
|
15
|
+
export declare enum BaseErrorCode {
|
|
16
|
+
/** Access denied due to invalid credentials or lack of authentication. */
|
|
17
|
+
UNAUTHORIZED = "UNAUTHORIZED",
|
|
18
|
+
/** Access denied despite valid authentication, due to insufficient permissions. */
|
|
19
|
+
FORBIDDEN = "FORBIDDEN",
|
|
20
|
+
/** The requested resource or entity could not be found. */
|
|
21
|
+
NOT_FOUND = "NOT_FOUND",
|
|
22
|
+
/** The request could not be completed due to a conflict with the current state of the resource. */
|
|
23
|
+
CONFLICT = "CONFLICT",
|
|
24
|
+
/** The request failed due to invalid input parameters or data. */
|
|
25
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
26
|
+
/** The request was made with an invalid input. */
|
|
27
|
+
INVALID_INPUT = "INVALID_INPUT",
|
|
28
|
+
/** The request was made with a method that is not allowed for the resource. */
|
|
29
|
+
METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED",
|
|
30
|
+
/** An error occurred while parsing input data (e.g., date string, JSON). */
|
|
31
|
+
PARSING_ERROR = "PARSING_ERROR",
|
|
32
|
+
/** The request was rejected because the client has exceeded rate limits. */
|
|
33
|
+
RATE_LIMITED = "RATE_LIMITED",
|
|
34
|
+
/** The request timed out before a response could be generated. */
|
|
35
|
+
TIMEOUT = "TIMEOUT",
|
|
36
|
+
/** The service is temporarily unavailable, possibly due to maintenance or overload. */
|
|
37
|
+
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE",
|
|
38
|
+
/** The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. */
|
|
39
|
+
GATEWAY_TIMEOUT = "GATEWAY_TIMEOUT",
|
|
40
|
+
/** A generic error occurred when interacting with an external service. */
|
|
41
|
+
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR",
|
|
42
|
+
/** An unexpected error occurred on the server side. */
|
|
43
|
+
INTERNAL_ERROR = "INTERNAL_ERROR",
|
|
44
|
+
/** An error occurred, but the specific cause is unknown or cannot be categorized. */
|
|
45
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
46
|
+
/** An error occurred during the loading or validation of configuration data. */
|
|
47
|
+
CONFIGURATION_ERROR = "CONFIGURATION_ERROR",
|
|
48
|
+
/** An error occurred during the initialization phase of a service or module. */
|
|
49
|
+
INITIALIZATION_FAILED = "INITIALIZATION_FAILED",
|
|
50
|
+
/** A service was used before it was properly initialized. */
|
|
51
|
+
SERVICE_NOT_INITIALIZED = "SERVICE_NOT_INITIALIZED",
|
|
52
|
+
/** A generic error occurred during a database operation. */
|
|
53
|
+
DATABASE_ERROR = "DATABASE_ERROR",
|
|
54
|
+
/** An error occurred while loading or interacting with an extension. */
|
|
55
|
+
EXTENSION_ERROR = "EXTENSION_ERROR",
|
|
56
|
+
/** An error occurred during the shutdown phase of a service or module. */
|
|
57
|
+
SHUTDOWN_ERROR = "SHUTDOWN_ERROR",
|
|
58
|
+
/** A generic error occurred during the execution of an agent's task. */
|
|
59
|
+
AGENT_EXECUTION_ERROR = "AGENT_EXECUTION_ERROR"
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Custom error class for MCP-specific errors, extending the built-in `Error` class.
|
|
63
|
+
* It standardizes error reporting by encapsulating a `BaseErrorCode`, a descriptive
|
|
64
|
+
* human-readable message, and optional structured details for more context.
|
|
65
|
+
*
|
|
66
|
+
* This class is central to error handling within the MCP framework, allowing for
|
|
67
|
+
* consistent error creation and propagation.
|
|
68
|
+
*/
|
|
69
|
+
export declare class McpError extends Error {
|
|
70
|
+
/**
|
|
71
|
+
* The standardized error code from {@link BaseErrorCode}.
|
|
72
|
+
*/
|
|
73
|
+
readonly code: BaseErrorCode;
|
|
74
|
+
/**
|
|
75
|
+
* Optional additional details or context about the error.
|
|
76
|
+
* This can be any structured data that helps in understanding or debugging the error.
|
|
77
|
+
*/
|
|
78
|
+
readonly details?: Record<string, unknown>;
|
|
79
|
+
/**
|
|
80
|
+
* Creates an instance of McpError.
|
|
81
|
+
*
|
|
82
|
+
* @param code - The standardized error code that categorizes the error.
|
|
83
|
+
* @param message - A human-readable description of the error.
|
|
84
|
+
* @param details - Optional. A record containing additional structured details about the error.
|
|
85
|
+
*/
|
|
86
|
+
constructor(code: BaseErrorCode, message: string, details?: Record<string, unknown>);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Zod schema for validating error objects. This schema can be used for:
|
|
90
|
+
* - Validating error structures when parsing error responses from external services.
|
|
91
|
+
* - Ensuring consistency when creating or handling error objects internally.
|
|
92
|
+
* - Generating TypeScript types for error objects.
|
|
93
|
+
*
|
|
94
|
+
* The schema enforces the presence of a `code` (from {@link BaseErrorCode}) and a `message`,
|
|
95
|
+
* and allows for optional `details`.
|
|
96
|
+
*/
|
|
97
|
+
export declare const ErrorSchema: z.ZodObject<{
|
|
98
|
+
/**
|
|
99
|
+
* The error code, corresponding to one of the {@link BaseErrorCode} enum values.
|
|
100
|
+
* This field is required and helps in programmatically identifying the error type.
|
|
101
|
+
*/
|
|
102
|
+
code: z.ZodNativeEnum<typeof BaseErrorCode>;
|
|
103
|
+
/**
|
|
104
|
+
* A human-readable, descriptive message explaining the error.
|
|
105
|
+
* This field is required and provides context to developers or users.
|
|
106
|
+
*/
|
|
107
|
+
message: z.ZodString;
|
|
108
|
+
/**
|
|
109
|
+
* Optional. A record containing additional structured details or context about the error.
|
|
110
|
+
* This can include things like invalid field names, specific values that caused issues, or other relevant data.
|
|
111
|
+
*/
|
|
112
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
113
|
+
}, "strip", z.ZodTypeAny, {
|
|
114
|
+
code: BaseErrorCode;
|
|
115
|
+
message: string;
|
|
116
|
+
details?: Record<string, unknown> | undefined;
|
|
117
|
+
}, {
|
|
118
|
+
code: BaseErrorCode;
|
|
119
|
+
message: string;
|
|
120
|
+
details?: Record<string, unknown> | undefined;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* TypeScript type inferred from the {@link ErrorSchema}.
|
|
124
|
+
* This type represents the structure of a validated error object, commonly used
|
|
125
|
+
* for error responses or when passing error information within the application.
|
|
126
|
+
*/
|
|
127
|
+
export type ErrorResponse = z.infer<typeof ErrorSchema>;
|