rhombus-node-mcp 0.1.35 → 0.1.43
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/dist/api/get-accessible-apps.js +7 -6
- package/dist/auth-context.js +2 -0
- package/dist/filtering-utils.js +27 -1
- package/dist/network/network.js +24 -26
- package/dist/transports/streamable-http.js +146 -169
- package/package.json +1 -4
|
@@ -2,15 +2,18 @@ import { logger } from "../logger.js";
|
|
|
2
2
|
import { postApi } from "../network/network.js";
|
|
3
3
|
/**
|
|
4
4
|
* Fetches `user.accessibleRhombusApps` from getCurrentUser for the given session.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Successful results are cached in-memory for the lifetime of the session;
|
|
6
|
+
* failures are NOT cached so transient errors don't poison the session.
|
|
7
|
+
* Returns null on error or missing session; callers should fall back to a
|
|
8
|
+
* permissive default.
|
|
7
9
|
*/
|
|
8
10
|
const cache = new Map();
|
|
9
11
|
export async function resolveAccessibleApps(sessionId) {
|
|
10
12
|
if (!sessionId)
|
|
11
13
|
return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
const cached = cache.get(sessionId);
|
|
15
|
+
if (cached !== undefined)
|
|
16
|
+
return cached;
|
|
14
17
|
try {
|
|
15
18
|
const res = await postApi({
|
|
16
19
|
route: "/customer/getCurrentUser",
|
|
@@ -19,7 +22,6 @@ export async function resolveAccessibleApps(sessionId) {
|
|
|
19
22
|
});
|
|
20
23
|
if (res.error) {
|
|
21
24
|
logger.warn(`resolveAccessibleApps: getCurrentUser failed for session ${sessionId}`);
|
|
22
|
-
cache.set(sessionId, null);
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
const apps = (res.user?.accessibleRhombusApps ?? []).filter((a) => a !== null && a !== undefined);
|
|
@@ -29,7 +31,6 @@ export async function resolveAccessibleApps(sessionId) {
|
|
|
29
31
|
}
|
|
30
32
|
catch (e) {
|
|
31
33
|
logger.warn(`resolveAccessibleApps: error for session ${sessionId}: ${String(e)}`);
|
|
32
|
-
cache.set(sessionId, null);
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
}
|
package/dist/filtering-utils.js
CHANGED
|
@@ -295,9 +295,35 @@ export function createFilteringProxy(server, blacklist = new Set()) {
|
|
|
295
295
|
if (blacklist.has(name)) {
|
|
296
296
|
return target.registerTool(name, config, handler);
|
|
297
297
|
}
|
|
298
|
+
let descriptionSuffix = FILTERING_DESCRIPTION_SUFFIX;
|
|
299
|
+
if (config.outputSchema) {
|
|
300
|
+
try {
|
|
301
|
+
let schema;
|
|
302
|
+
if (config.outputSchema instanceof z.ZodType) {
|
|
303
|
+
schema = config.outputSchema;
|
|
304
|
+
}
|
|
305
|
+
else if (typeof config.outputSchema === "object") {
|
|
306
|
+
schema = z.object(config.outputSchema);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
schema = config.outputSchema;
|
|
310
|
+
}
|
|
311
|
+
const paths = zodToDotNotationPaths(schema);
|
|
312
|
+
if (paths.length > 0) {
|
|
313
|
+
const filteredPaths = [...new Set(paths.filter((p) => p !== "requestType" && p !== "error" && p.trim() !== ""))].sort();
|
|
314
|
+
if (filteredPaths.length > 0) {
|
|
315
|
+
descriptionSuffix += `\n\n**Available output field paths for this tool's \`includeFields\` / \`filterBy\`:**\n` +
|
|
316
|
+
filteredPaths.map((p) => `- \`"${p}"\``).join("\n");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
// Fall back to the default description suffix on parsing failure
|
|
322
|
+
}
|
|
323
|
+
}
|
|
298
324
|
const augmentedConfig = {
|
|
299
325
|
...config,
|
|
300
|
-
description: (config.description ?? "") +
|
|
326
|
+
description: (config.description ?? "") + descriptionSuffix,
|
|
301
327
|
inputSchema: {
|
|
302
328
|
...config.inputSchema,
|
|
303
329
|
includeFields: INCLUDE_FIELDS_ARG,
|
package/dist/network/network.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "../logger.js";
|
|
2
|
-
import {
|
|
2
|
+
import { requestAuthContext } from "../auth-context.js";
|
|
3
3
|
export const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
|
|
4
4
|
export const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
|
|
5
5
|
export const BASE_URL = `https://${serverUrl}/api`;
|
|
@@ -26,50 +26,48 @@ export const appendQueryParams = (url, params) => {
|
|
|
26
26
|
const queryString = existingSearchParams.toString();
|
|
27
27
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
28
28
|
};
|
|
29
|
-
export function constructRequestHeaders(url, modifiers, sessionId
|
|
30
|
-
|
|
29
|
+
export function constructRequestHeaders(url, modifiers, sessionId // kept for API compatibility; ignored — always uses AsyncLocalStorage
|
|
30
|
+
) {
|
|
31
|
+
// construct auth headers from async context (stateless: set per-request by the transport handler)
|
|
31
32
|
let authHeaders = {};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// use sessionId to get auth
|
|
38
|
-
const auth = authStore.get(sessionId);
|
|
39
|
-
if (!auth) {
|
|
40
|
-
logger.error(`No auth found for sessionId: ${sessionId}`);
|
|
41
|
-
throw new Error(`No auth found for sessionId: ${sessionId}`);
|
|
42
|
-
}
|
|
43
|
-
if ("oauthToken" in auth) {
|
|
33
|
+
const contextAuth = requestAuthContext.getStore();
|
|
34
|
+
if (contextAuth) {
|
|
35
|
+
if ("oauthBearer" in contextAuth) {
|
|
36
|
+
// The Bearer is an opaque Rhombus access token issued by the Rhombus
|
|
37
|
+
// OAuth 2.1 authorization server. api2 validates it directly.
|
|
44
38
|
authHeaders = {
|
|
45
|
-
|
|
39
|
+
"x-auth-access-token": contextAuth.oauthBearer,
|
|
46
40
|
"x-auth-scheme": "api-oauth-token",
|
|
47
41
|
};
|
|
48
42
|
}
|
|
49
|
-
else if ("apiKey" in
|
|
43
|
+
else if ("apiKey" in contextAuth) {
|
|
50
44
|
authHeaders = {
|
|
51
|
-
"x-auth-apikey":
|
|
45
|
+
"x-auth-apikey": contextAuth.apiKey,
|
|
52
46
|
"x-auth-scheme": "api-token",
|
|
53
47
|
};
|
|
54
48
|
}
|
|
55
|
-
else if ("sessionId" in
|
|
49
|
+
else if ("sessionId" in contextAuth) {
|
|
56
50
|
authHeaders = {
|
|
57
|
-
"x-auth-session":
|
|
58
|
-
"x-auth-chat":
|
|
51
|
+
"x-auth-session": contextAuth.sessionId,
|
|
52
|
+
"x-auth-chat": contextAuth.latestRecordUuid,
|
|
59
53
|
"x-auth-scheme": "chatbot",
|
|
60
54
|
};
|
|
61
|
-
url = appendQueryParams(url, { _rs:
|
|
55
|
+
url = appendQueryParams(url, { _rs: contextAuth.sessionId });
|
|
62
56
|
}
|
|
63
|
-
else if ("cookie" in
|
|
57
|
+
else if ("cookie" in contextAuth) {
|
|
64
58
|
authHeaders = {
|
|
65
59
|
"x-auth-scheme": "web2",
|
|
66
|
-
cookie:
|
|
60
|
+
cookie: contextAuth.cookie,
|
|
67
61
|
};
|
|
68
|
-
if (
|
|
69
|
-
url = appendQueryParams(url, { _rs:
|
|
62
|
+
if (contextAuth.sessionAlias) {
|
|
63
|
+
url = appendQueryParams(url, { _rs: contextAuth.sessionAlias });
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
}
|
|
67
|
+
else {
|
|
68
|
+
// no async context — fall back to env API key (local dev / stdio)
|
|
69
|
+
authHeaders = AUTH_HEADERS;
|
|
70
|
+
}
|
|
73
71
|
// merge headers
|
|
74
72
|
const requestHeaders = {
|
|
75
73
|
...STATIC_HEADERS,
|
|
@@ -1,211 +1,188 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
2
|
import cors from "cors";
|
|
5
3
|
import express from "express";
|
|
6
|
-
import {
|
|
4
|
+
import { requestAuthContext } from "../auth-context.js";
|
|
7
5
|
import createServer from "../createServer.js";
|
|
8
6
|
import { logger } from "../logger.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// x-auth-* header extraction (internal chatbot / API key clients) — UNCHANGED
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
9
10
|
var AuthScheme;
|
|
10
11
|
(function (AuthScheme) {
|
|
11
|
-
AuthScheme["OAUTH"] = "oauth";
|
|
12
12
|
AuthScheme["API_TOKEN"] = "api-token";
|
|
13
13
|
AuthScheme["CHATBOT"] = "chatbot";
|
|
14
14
|
AuthScheme["WEB2"] = "web2";
|
|
15
15
|
})(AuthScheme || (AuthScheme = {}));
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function populateAuthStore(req, sessionId) {
|
|
24
|
-
const oauthToken = req.headers["x-auth-access-token"];
|
|
25
|
-
const authScheme = req.headers["x-auth-scheme"] ?? AuthScheme.API_TOKEN;
|
|
26
|
-
if (oauthToken && typeof oauthToken === "string") {
|
|
27
|
-
authStore.set(sessionId, { oauthToken, createdMs: Date.now() });
|
|
28
|
-
logger.info(`🔒 MCP request authenticated with oauth token (session ${sessionId})`);
|
|
29
|
-
return true;
|
|
16
|
+
function extractAuth(req) {
|
|
17
|
+
const scheme = req.headers["x-auth-scheme"] ?? AuthScheme.API_TOKEN;
|
|
18
|
+
if (scheme === AuthScheme.API_TOKEN) {
|
|
19
|
+
const apiKey = req.headers["x-auth-apikey"] ?? process.env.RHOMBUS_API_KEY;
|
|
20
|
+
if (!apiKey)
|
|
21
|
+
return null;
|
|
22
|
+
return { apiKey };
|
|
30
23
|
}
|
|
31
|
-
if (
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
logger.info(`🔒 MCP request authenticated with api key (session ${sessionId})`);
|
|
40
|
-
authStore.set(sessionId, { apiKey, createdMs: Date.now() });
|
|
41
|
-
return true;
|
|
24
|
+
if (scheme === AuthScheme.CHATBOT) {
|
|
25
|
+
const sessionId = req.headers["x-auth-session"];
|
|
26
|
+
const latestRecordUuid = req.headers["x-auth-chat"];
|
|
27
|
+
if (!sessionId || !latestRecordUuid)
|
|
28
|
+
return null;
|
|
29
|
+
return { sessionId, latestRecordUuid };
|
|
42
30
|
}
|
|
43
|
-
if (
|
|
44
|
-
"x-auth-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
sessionId: req.headers["x-auth-session"],
|
|
49
|
-
latestRecordUuid: req.headers["x-auth-chat"],
|
|
50
|
-
createdMs: Date.now(),
|
|
51
|
-
});
|
|
52
|
-
return true;
|
|
31
|
+
if (scheme === AuthScheme.WEB2) {
|
|
32
|
+
const cookie = req.headers["x-auth-cookie"];
|
|
33
|
+
if (!cookie)
|
|
34
|
+
return null;
|
|
35
|
+
return { cookie, sessionAlias: req.headers["x-auth-session-alias"] };
|
|
53
36
|
}
|
|
54
|
-
|
|
55
|
-
logger.info(`🔒 MCP request authenticated with x-auth-cookie (session ${sessionId})`);
|
|
56
|
-
authStore.set(sessionId, {
|
|
57
|
-
cookie: req.headers["x-auth-cookie"],
|
|
58
|
-
sessionAlias: req.headers["x-auth-session-alias"],
|
|
59
|
-
createdMs: Date.now(),
|
|
60
|
-
});
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
logger.warn(`populateAuthStore: invalid auth scheme. x-auth-scheme: ${req.headers["x-auth-scheme"]}, x-auth-session: ${req.headers["x-auth-session"]}, x-auth-chat: ${req.headers["x-auth-chat"]}`);
|
|
64
|
-
return false;
|
|
37
|
+
return null;
|
|
65
38
|
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Transport
|
|
41
|
+
//
|
|
42
|
+
// Pure RFC 9728 OAuth 2.1 Resource Server. The Rhombus authorization server
|
|
43
|
+
// (RFC 8414 issuer) is configured via OAUTH_AS_ISSUER_URL — e.g. set it to
|
|
44
|
+
// the Rhombus auth host whose /.well-known/oauth-authorization-server
|
|
45
|
+
// document advertises /authorize, /token, /register, /revoke per
|
|
46
|
+
// RFC 6749 + 7636 + 7591 + 7009.
|
|
47
|
+
//
|
|
48
|
+
// The AS issues opaque Rhombus access tokens, so the MCP server does no
|
|
49
|
+
// local validation — it just forwards the Bearer to api2 as
|
|
50
|
+
// x-auth-access-token, which api2 already knows how to validate.
|
|
51
|
+
//
|
|
52
|
+
// The legacy x-auth-* dispatch for internal callers (chatbot, API-key, web2)
|
|
53
|
+
// is unchanged.
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
66
55
|
export default function streamableHttpTransport() {
|
|
67
56
|
const app = express();
|
|
68
57
|
app.use(express.json());
|
|
58
|
+
const oauthAsIssuerUrl = process.env.OAUTH_AS_ISSUER_URL;
|
|
59
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL;
|
|
60
|
+
const allowedHost = process.env.ALLOWED_HOST;
|
|
61
|
+
const allowedHosts = allowedHost
|
|
62
|
+
? allowedHost
|
|
63
|
+
.split(",")
|
|
64
|
+
.map(h => h.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
: [];
|
|
69
67
|
app.use(cors({
|
|
70
|
-
// TODO: domain
|
|
71
68
|
origin: ["*"],
|
|
72
69
|
exposedHeaders: ["mcp-session-id"],
|
|
73
|
-
allowedHeaders: [
|
|
70
|
+
allowedHeaders: [
|
|
71
|
+
"Content-Type",
|
|
72
|
+
"Authorization",
|
|
73
|
+
"mcp-session-id",
|
|
74
|
+
"x-auth-scheme",
|
|
75
|
+
"x-auth-apikey",
|
|
76
|
+
"x-auth-session",
|
|
77
|
+
"x-auth-chat",
|
|
78
|
+
"x-auth-cookie",
|
|
79
|
+
"x-auth-session-alias",
|
|
80
|
+
],
|
|
74
81
|
}));
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
82
|
+
app.get("/health", (_, res) => {
|
|
83
|
+
res.status(200).json({ status: "ok" });
|
|
84
|
+
});
|
|
85
|
+
if (oauthAsIssuerUrl) {
|
|
86
|
+
logger.info(`OAuth Resource Server — AS at ${oauthAsIssuerUrl}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
logger.info("OAUTH_AS_ISSUER_URL not set — Bearer tokens will be rejected. Set this to the Rhombus authorization server issuer URL (e.g. https://auth-web.<env>.rhombussystems.com/).");
|
|
90
|
+
}
|
|
91
|
+
// RFC 9728 — Protected Resource Metadata. Points clients at the Rhombus AS.
|
|
92
|
+
// Preserves the issuer URL verbatim so the value matches the `issuer` field
|
|
93
|
+
// strict OAuth clients (Claude Desktop, etc.) read from the AS metadata.
|
|
94
|
+
app.get("/.well-known/oauth-protected-resource", (req, res) => {
|
|
95
|
+
if (!oauthAsIssuerUrl) {
|
|
96
|
+
res.status(404).json({ error: "oauth_not_configured" });
|
|
97
|
+
return;
|
|
91
98
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
res.json({
|
|
100
|
+
resource: mcpServerUrl ?? `${getSelfOrigin(req, mcpServerUrl)}/mcp`,
|
|
101
|
+
authorization_servers: [oauthAsIssuerUrl],
|
|
102
|
+
scopes_supported: ["rhombus:access"],
|
|
103
|
+
bearer_methods_supported: ["header"],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
const handleMcpRequest = async (req, res) => {
|
|
107
|
+
logger.info("Received MCP request");
|
|
108
|
+
let auth = null;
|
|
109
|
+
const authHeader = req.headers.authorization;
|
|
110
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
111
|
+
if (!oauthAsIssuerUrl) {
|
|
112
|
+
return reject401(req, res, mcpServerUrl, "OAuth not configured: set OAUTH_AS_ISSUER_URL");
|
|
105
113
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
|
|
113
|
-
// locally, make sure to set:
|
|
114
|
-
// enableDnsRebindingProtection: true,
|
|
115
|
-
// allowedHosts: ['127.0.0.1'],
|
|
116
|
-
});
|
|
117
|
-
// Clean up transport when closed
|
|
118
|
-
transport.onclose = () => {
|
|
119
|
-
if (transport.sessionId) {
|
|
120
|
-
transports.delete(transport.sessionId);
|
|
121
|
-
authStore.delete(transport.sessionId);
|
|
122
|
-
clearAccessibleAppsCache(transport.sessionId);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
// newSessionId is already in authStore; createServer can resolve
|
|
126
|
-
// accessibleRhombusApps and pick the right tool set.
|
|
127
|
-
const server = await createServer({ sessionId: newSessionId });
|
|
128
|
-
// Connect to the MCP server
|
|
129
|
-
await server.connect(transport);
|
|
130
|
-
logger.info(`🔗 Transport connected with sessionId: ${newSessionId}`);
|
|
114
|
+
const token = authHeader.slice(7).trim();
|
|
115
|
+
if (!token) {
|
|
116
|
+
return reject401(req, res, mcpServerUrl, "empty Bearer token");
|
|
117
|
+
}
|
|
118
|
+
auth = { oauthBearer: token };
|
|
119
|
+
logger.info("MCP request authenticated via Bearer (opaque, forwarded to api2)");
|
|
131
120
|
}
|
|
132
121
|
else {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
message: "Bad Request: No valid session ID provided",
|
|
139
|
-
},
|
|
140
|
-
id: null,
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
122
|
+
auth = extractAuth(req);
|
|
123
|
+
if (!auth) {
|
|
124
|
+
return reject401(req, res, mcpServerUrl, "no credentials presented");
|
|
125
|
+
}
|
|
126
|
+
logger.info("MCP request authenticated via x-auth-* headers");
|
|
143
127
|
}
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
id: null,
|
|
155
|
-
}));
|
|
156
|
-
});
|
|
157
|
-
app.delete("/mcp", async (req, res) => {
|
|
158
|
-
logger.warn("Received Not Allowed DELETE MCP request");
|
|
159
|
-
res.writeHead(405).end(JSON.stringify({
|
|
160
|
-
jsonrpc: "2.0",
|
|
161
|
-
error: {
|
|
162
|
-
code: -32000,
|
|
163
|
-
message: "Method not allowed.",
|
|
164
|
-
},
|
|
165
|
-
id: null,
|
|
166
|
-
}));
|
|
167
|
-
});
|
|
168
|
-
/**
|
|
169
|
-
* STATELESS ENDPOINT
|
|
170
|
-
*/
|
|
171
|
-
app.post("/mcp-stateless", async (req, res) => {
|
|
172
|
-
logger.info(`Received stateless MCP request`, req.body);
|
|
173
|
-
let transport;
|
|
174
|
-
transport = new StreamableHTTPServerTransport({
|
|
175
|
-
sessionIdGenerator: undefined,
|
|
128
|
+
await requestAuthContext.run(auth, async () => {
|
|
129
|
+
const transport = new StreamableHTTPServerTransport({
|
|
130
|
+
sessionIdGenerator: undefined,
|
|
131
|
+
...(allowedHosts.length > 0 ? { enableDnsRebindingProtection: true, allowedHosts } : {}),
|
|
132
|
+
});
|
|
133
|
+
const server = await createServer();
|
|
134
|
+
await server.connect(transport);
|
|
135
|
+
logger.info("🔗 Stateless MCP Transport connected");
|
|
136
|
+
await transport.handleRequest(req, res, req.body);
|
|
176
137
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
logger.info(`🔗 Stateless Transport connected`);
|
|
181
|
-
// Handle the request
|
|
182
|
-
await transport.handleRequest(req, res, req.body);
|
|
183
|
-
});
|
|
184
|
-
app.get("/mcp-stateless", async (req, res) => {
|
|
185
|
-
logger.warn("Received Not Allowed GET MCP request");
|
|
138
|
+
};
|
|
139
|
+
app.post("/mcp", handleMcpRequest);
|
|
140
|
+
app.get("/mcp", (_, res) => {
|
|
186
141
|
res.writeHead(405).end(JSON.stringify({
|
|
187
142
|
jsonrpc: "2.0",
|
|
188
|
-
error: {
|
|
189
|
-
code: -32000,
|
|
190
|
-
message: "Method not allowed.",
|
|
191
|
-
},
|
|
143
|
+
error: { code: -32000, message: "Method not allowed." },
|
|
192
144
|
id: null,
|
|
193
145
|
}));
|
|
194
146
|
});
|
|
195
|
-
app.delete("/mcp
|
|
196
|
-
logger.warn("Received Not Allowed DELETE MCP request");
|
|
147
|
+
app.delete("/mcp", (_, res) => {
|
|
197
148
|
res.writeHead(405).end(JSON.stringify({
|
|
198
149
|
jsonrpc: "2.0",
|
|
199
|
-
error: {
|
|
200
|
-
code: -32000,
|
|
201
|
-
message: "Method not allowed.",
|
|
202
|
-
},
|
|
150
|
+
error: { code: -32000, message: "Method not allowed." },
|
|
203
151
|
id: null,
|
|
204
152
|
}));
|
|
205
153
|
});
|
|
206
|
-
|
|
207
|
-
const PORT = process.env.PORT || 3000;
|
|
154
|
+
const PORT = process.env.PORT ?? 3000;
|
|
208
155
|
app.listen(PORT, () => {
|
|
209
156
|
logger.info(`rhombus-node-mcp listening on port ${PORT}`);
|
|
210
157
|
});
|
|
211
158
|
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Helpers
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
function reject401(req, res, mcpServerUrl, msg) {
|
|
163
|
+
const resourceMetadataUrl = `${getSelfOrigin(req, mcpServerUrl)}/.well-known/oauth-protected-resource`;
|
|
164
|
+
res
|
|
165
|
+
.status(401)
|
|
166
|
+
.set("WWW-Authenticate", `Bearer error="invalid_token", error_description="${escapeWwwAuth(msg)}", resource_metadata="${resourceMetadataUrl}"`)
|
|
167
|
+
.json({
|
|
168
|
+
jsonrpc: "2.0",
|
|
169
|
+
error: { code: -32000, message: `Unauthorized: ${msg}` },
|
|
170
|
+
id: null,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function getSelfOrigin(req, mcpServerUrl) {
|
|
174
|
+
if (mcpServerUrl) {
|
|
175
|
+
try {
|
|
176
|
+
return new URL(mcpServerUrl).origin;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// fall through
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const proto = req.headers["x-forwarded-proto"] ?? req.protocol;
|
|
183
|
+
const host = req.headers.host ?? "localhost";
|
|
184
|
+
return `${proto}://${host}`;
|
|
185
|
+
}
|
|
186
|
+
function escapeWwwAuth(s) {
|
|
187
|
+
return s.replace(/["\r\n]/g, "");
|
|
188
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhombus-node-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "MCP server for Rhombus API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -56,9 +56,7 @@
|
|
|
56
56
|
"cors": "^2.8.5",
|
|
57
57
|
"dotenv": "^16.5.0",
|
|
58
58
|
"express": "^5.1.0",
|
|
59
|
-
"express-jwt": "^8.5.1",
|
|
60
59
|
"faiss-node": "^0.5.1",
|
|
61
|
-
"jsonwebtoken": "^9.0.2",
|
|
62
60
|
"langchain": "^0.3.30",
|
|
63
61
|
"log4js": "^6.9.1",
|
|
64
62
|
"luxon": "^3.6.1",
|
|
@@ -69,7 +67,6 @@
|
|
|
69
67
|
"devDependencies": {
|
|
70
68
|
"@types/cors": "^2.8.19",
|
|
71
69
|
"@types/express": "^5.0.3",
|
|
72
|
-
"@types/jwt-express": "^1.1.6",
|
|
73
70
|
"@types/luxon": "^3.6.2",
|
|
74
71
|
"@types/node": "^22.14.0",
|
|
75
72
|
"openapi-typescript-codegen": "^0.29.0",
|