@nexusm/mcp-server 0.1.0
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 +21 -0
- package/README.md +13 -0
- package/RUNBOOK.md +190 -0
- package/dist/auth.d.ts +49 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +62 -0
- package/dist/auth.js.map +1 -0
- package/dist/errors.d.ts +211 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +245 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +146 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +245 -0
- package/dist/metrics.js.map +1 -0
- package/dist/tools/context.d.ts +48 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +229 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory_create.d.ts +37 -0
- package/dist/tools/memory_create.d.ts.map +1 -0
- package/dist/tools/memory_create.js +242 -0
- package/dist/tools/memory_create.js.map +1 -0
- package/dist/tools/memory_feedback.d.ts +44 -0
- package/dist/tools/memory_feedback.d.ts.map +1 -0
- package/dist/tools/memory_feedback.js +259 -0
- package/dist/tools/memory_feedback.js.map +1 -0
- package/dist/tools/memory_search.d.ts +44 -0
- package/dist/tools/memory_search.d.ts.map +1 -0
- package/dist/tools/memory_search.js +160 -0
- package/dist/tools/memory_search.js.map +1 -0
- package/dist/tools/types.d.ts +36 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +29 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +50 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error contract and HTTP→MCP mapping for the Nexusm MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Wave 1 (TASK-007): declared the type surface — NexusError, McpErrorCode,
|
|
5
|
+
* interface stubs for AuthError / NetworkError / CancelError.
|
|
6
|
+
* Wave 2B (TASK-013): implements the full HTTP-status → MCP-error-code
|
|
7
|
+
* mapping (proposal §M-3) via `mapHttpStatusToMcpError` and
|
|
8
|
+
* `isAxiosLikeError`.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY (matches auth.ts discipline):
|
|
11
|
+
* `toJSON()` deliberately omits `cause` and `stack`. An axios-style error
|
|
12
|
+
* attached as `cause` typically carries the original request config
|
|
13
|
+
* including the `Authorization: Bearer <token>` header. Leaking that via a
|
|
14
|
+
* JSON.stringify of a NexusError would defeat the token-redaction guarantee
|
|
15
|
+
* of auth.ts. If callers need to inspect the cause they must do so
|
|
16
|
+
* explicitly, not via serialization.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* MCP / JSON-RPC error codes surfaced by this server.
|
|
20
|
+
*
|
|
21
|
+
* Values mirror `@modelcontextprotocol/sdk` `ErrorCode` enum
|
|
22
|
+
* (`dist/esm/types.d.ts`). We re-declare locally rather than re-export
|
|
23
|
+
* the SDK enum so that:
|
|
24
|
+
* 1. errors.ts has zero runtime import from the SDK (keeps the
|
|
25
|
+
* contract layer independent of SDK version churn)
|
|
26
|
+
* 2. TASK-013's mapping logic and tests have a single source of truth
|
|
27
|
+
* for which codes this server is allowed to emit
|
|
28
|
+
*
|
|
29
|
+
* Scope decision (resolved ambiguity from spec):
|
|
30
|
+
* We enumerate the four JSON-RPC standard codes required by §M-3
|
|
31
|
+
* (`InvalidRequest`, `MethodNotFound`, `InvalidParams`, `InternalError`),
|
|
32
|
+
* plus `ParseError` (-32700) for completeness of the JSON-RPC base
|
|
33
|
+
* set, plus `ConnectionClosed` (-32000) and `RequestTimeout` (-32001)
|
|
34
|
+
* which the SDK defines and which `NetworkError` / `CancelError`
|
|
35
|
+
* downstream mappings will need. UrlElicitationRequired (-32042) is
|
|
36
|
+
* intentionally omitted — not in scope for Wave 1 / Wave 2.
|
|
37
|
+
*/
|
|
38
|
+
export var McpErrorCode;
|
|
39
|
+
(function (McpErrorCode) {
|
|
40
|
+
// JSON-RPC standard (https://www.jsonrpc.org/specification#error_object)
|
|
41
|
+
McpErrorCode[McpErrorCode["ParseError"] = -32700] = "ParseError";
|
|
42
|
+
McpErrorCode[McpErrorCode["InvalidRequest"] = -32600] = "InvalidRequest";
|
|
43
|
+
McpErrorCode[McpErrorCode["MethodNotFound"] = -32601] = "MethodNotFound";
|
|
44
|
+
McpErrorCode[McpErrorCode["InvalidParams"] = -32602] = "InvalidParams";
|
|
45
|
+
McpErrorCode[McpErrorCode["InternalError"] = -32603] = "InternalError";
|
|
46
|
+
// MCP SDK extensions used by this server's error taxonomy
|
|
47
|
+
McpErrorCode[McpErrorCode["ConnectionClosed"] = -32000] = "ConnectionClosed";
|
|
48
|
+
McpErrorCode[McpErrorCode["RequestTimeout"] = -32001] = "RequestTimeout";
|
|
49
|
+
/**
|
|
50
|
+
* TASK-013 additions: custom codes in the application-defined range
|
|
51
|
+
* (-32099..-32000 is reserved for implementation; we use the next
|
|
52
|
+
* available slots above -32000 for semantic clarity).
|
|
53
|
+
*
|
|
54
|
+
* Unauthorized (-32011): 401 / 403 from Nexus REST — semantically distinct
|
|
55
|
+
* from InvalidRequest (-32600) so clients can detect auth failures without
|
|
56
|
+
* parsing the message string.
|
|
57
|
+
* RateLimited (-32012): 429 Retry-After. Clients should honor
|
|
58
|
+
* `data.retry_after_seconds` before retrying.
|
|
59
|
+
*/
|
|
60
|
+
McpErrorCode[McpErrorCode["Unauthorized"] = -32011] = "Unauthorized";
|
|
61
|
+
McpErrorCode[McpErrorCode["RateLimited"] = -32012] = "RateLimited";
|
|
62
|
+
})(McpErrorCode || (McpErrorCode = {}));
|
|
63
|
+
/**
|
|
64
|
+
* Base error for all errors this MCP server emits.
|
|
65
|
+
*
|
|
66
|
+
* Carries enough structured context to translate a thrown `NexusError` into a
|
|
67
|
+
* JSON-RPC error response without re-inspecting the underlying axios / SDK
|
|
68
|
+
* error.
|
|
69
|
+
*/
|
|
70
|
+
export class NexusError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* Upstream HTTP status (Nexus REST), or `null` when the error did not
|
|
73
|
+
* originate from an HTTP response (e.g. DNS failure, abort, internal
|
|
74
|
+
* invariant violation).
|
|
75
|
+
*/
|
|
76
|
+
httpStatus;
|
|
77
|
+
/** MCP/JSON-RPC error code that this error will surface as. */
|
|
78
|
+
mcpErrorCode;
|
|
79
|
+
/**
|
|
80
|
+
* Whether the MCP client may safely retry this request.
|
|
81
|
+
* Populated by `mapHttpStatusToMcpError` and the NLI/network helpers.
|
|
82
|
+
*/
|
|
83
|
+
retryable;
|
|
84
|
+
/**
|
|
85
|
+
* Additional structured data surfaced in the JSON-RPC `error.data` field.
|
|
86
|
+
* Safe to serialize — must never contain auth tokens or raw SDK internals.
|
|
87
|
+
* Populated by the mapping layer (e.g. `retry_after_seconds`, `network`,
|
|
88
|
+
* `timeout`).
|
|
89
|
+
*/
|
|
90
|
+
data;
|
|
91
|
+
/**
|
|
92
|
+
* Underlying cause. Per ES2022 `Error.cause`. **Not serialized** by
|
|
93
|
+
* `toJSON()` — see file header SECURITY note.
|
|
94
|
+
*/
|
|
95
|
+
cause;
|
|
96
|
+
constructor(message, mcpErrorCode, httpStatus = null, cause, options) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = 'NexusError';
|
|
99
|
+
this.mcpErrorCode = mcpErrorCode;
|
|
100
|
+
this.httpStatus = httpStatus;
|
|
101
|
+
this.retryable = options?.retryable ?? false;
|
|
102
|
+
if (options?.data !== undefined) {
|
|
103
|
+
this.data = options.data;
|
|
104
|
+
}
|
|
105
|
+
if (cause !== undefined) {
|
|
106
|
+
this.cause = cause;
|
|
107
|
+
}
|
|
108
|
+
// Restore prototype chain — required when extending Error under
|
|
109
|
+
// some TS target/module combinations (defensive; cheap).
|
|
110
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Safe serialization. Deliberately omits `cause` and `stack` to
|
|
114
|
+
* prevent accidental token leakage if a caller logs the JSON form.
|
|
115
|
+
*
|
|
116
|
+
* `data` IS included — it is caller-controlled structured metadata that
|
|
117
|
+
* must never contain raw SDK objects (that would be caught during review
|
|
118
|
+
* of `mapHttpStatusToMcpError` callers).
|
|
119
|
+
*/
|
|
120
|
+
toJSON() {
|
|
121
|
+
const base = {
|
|
122
|
+
name: this.name,
|
|
123
|
+
message: this.message,
|
|
124
|
+
httpStatus: this.httpStatus,
|
|
125
|
+
mcpErrorCode: this.mcpErrorCode,
|
|
126
|
+
};
|
|
127
|
+
if (this.data !== undefined) {
|
|
128
|
+
base.data = this.data;
|
|
129
|
+
}
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Type guard for axios-compatible errors thrown by `@nexusm/sdk`.
|
|
135
|
+
*
|
|
136
|
+
* Matches any object with `isAxiosError === true`, which is the canonical
|
|
137
|
+
* axios duck-type flag. This guard intentionally does NOT import axios — it
|
|
138
|
+
* keeps `errors.ts` free of SDK runtime dependencies.
|
|
139
|
+
*/
|
|
140
|
+
export function isAxiosLikeError(err) {
|
|
141
|
+
return (typeof err === 'object' &&
|
|
142
|
+
err !== null &&
|
|
143
|
+
err['isAxiosError'] === true);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a Retry-After header value into seconds.
|
|
147
|
+
*
|
|
148
|
+
* Handles both integer-seconds form ("60") and HTTP-date form
|
|
149
|
+
* ("Wed, 21 Oct 2026 07:28:00 GMT"). Returns `undefined` if the header
|
|
150
|
+
* is absent or unparseable — callers should degrade gracefully.
|
|
151
|
+
*/
|
|
152
|
+
function parseRetryAfterSeconds(headers) {
|
|
153
|
+
if (headers === undefined)
|
|
154
|
+
return undefined;
|
|
155
|
+
const raw = headers['retry-after'] ?? headers['Retry-After'];
|
|
156
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
// Integer seconds
|
|
160
|
+
const asInt = parseInt(value, 10);
|
|
161
|
+
if (!Number.isNaN(asInt) && String(asInt) === value.trim()) {
|
|
162
|
+
return asInt;
|
|
163
|
+
}
|
|
164
|
+
// HTTP-date: compute delta from now
|
|
165
|
+
const ts = Date.parse(value);
|
|
166
|
+
if (!Number.isNaN(ts)) {
|
|
167
|
+
const delta = Math.ceil((ts - Date.now()) / 1000);
|
|
168
|
+
return delta > 0 ? delta : 0;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Canonical entrypoint for converting an upstream HTTP response (or SDK
|
|
174
|
+
* network/timeout error) into a typed `NexusError` with the correct
|
|
175
|
+
* `McpErrorCode`, `retryable` flag, and `data` extras.
|
|
176
|
+
*
|
|
177
|
+
* Proposal §M-3 mapping table:
|
|
178
|
+
*
|
|
179
|
+
* | httpStatus | McpErrorCode | retryable | data extras |
|
|
180
|
+
* |-------------------------|-------------------|-----------|--------------------------|
|
|
181
|
+
* | 401, 403 | Unauthorized | false | — |
|
|
182
|
+
* | 404 | MethodNotFound | false | — |
|
|
183
|
+
* | 422 | InvalidParams | false | — |
|
|
184
|
+
* | 429 | RateLimited | true* | retry_after_seconds?: n |
|
|
185
|
+
* | 503 | ConnectionClosed | true | — |
|
|
186
|
+
* | 5xx (else) | InternalError | true | — |
|
|
187
|
+
* | null + network=true | InternalError | true | network: true |
|
|
188
|
+
* | null + timeout=true | RequestTimeout | true | timeout: true |
|
|
189
|
+
*
|
|
190
|
+
* *429: retryable "after Retry-After header elapses" — we set retryable=true
|
|
191
|
+
* and populate `data.retry_after_seconds` so clients can honour the window.
|
|
192
|
+
*
|
|
193
|
+
* Note: HTTP 200 + body.errors != null is NOT an error; that is the
|
|
194
|
+
* partial-degradation path handled in tool handlers (see context.ts). This
|
|
195
|
+
* function is only invoked on non-2xx responses or SDK error throws.
|
|
196
|
+
*
|
|
197
|
+
* @param httpStatus HTTP status code, or `null` for non-HTTP errors.
|
|
198
|
+
* @param body Raw response body (typed `unknown`; we do not parse it).
|
|
199
|
+
* @param headers Response headers, used only to extract Retry-After on 429.
|
|
200
|
+
*/
|
|
201
|
+
export function mapHttpStatusToMcpError(httpStatus, body, headers) {
|
|
202
|
+
// Non-HTTP origin: distinguish timeout from generic network failure by
|
|
203
|
+
// inspecting whether the caller passed { timeout: true } in body (we
|
|
204
|
+
// treat `body` as a hint bag for non-HTTP paths).
|
|
205
|
+
if (httpStatus === null) {
|
|
206
|
+
const hint = body;
|
|
207
|
+
if (hint?.['timeout'] === true) {
|
|
208
|
+
return new NexusError('Request timed out', McpErrorCode.RequestTimeout, null, undefined, {
|
|
209
|
+
retryable: true,
|
|
210
|
+
data: { timeout: true },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return new NexusError('Network error', McpErrorCode.InternalError, null, undefined, {
|
|
214
|
+
retryable: true,
|
|
215
|
+
data: { network: true },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
switch (true) {
|
|
219
|
+
case httpStatus === 401 || httpStatus === 403:
|
|
220
|
+
return new NexusError(`Unauthorized (HTTP ${httpStatus})`, McpErrorCode.Unauthorized, httpStatus, undefined, { retryable: false });
|
|
221
|
+
case httpStatus === 404:
|
|
222
|
+
return new NexusError('Resource not found (HTTP 404)', McpErrorCode.MethodNotFound, 404, undefined, { retryable: false });
|
|
223
|
+
case httpStatus === 422:
|
|
224
|
+
return new NexusError('Invalid parameters (HTTP 422)', McpErrorCode.InvalidParams, 422, undefined, { retryable: false });
|
|
225
|
+
case httpStatus === 429: {
|
|
226
|
+
const retryAfterSeconds = parseRetryAfterSeconds(headers);
|
|
227
|
+
const data = {};
|
|
228
|
+
if (retryAfterSeconds !== undefined) {
|
|
229
|
+
data['retry_after_seconds'] = retryAfterSeconds;
|
|
230
|
+
}
|
|
231
|
+
return new NexusError('Rate limited (HTTP 429)', McpErrorCode.RateLimited, 429, undefined, {
|
|
232
|
+
retryable: true,
|
|
233
|
+
data: Object.keys(data).length > 0 ? data : undefined,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
case httpStatus === 503:
|
|
237
|
+
return new NexusError('Service unavailable (HTTP 503)', McpErrorCode.ConnectionClosed, 503, undefined, { retryable: true });
|
|
238
|
+
case httpStatus >= 500:
|
|
239
|
+
return new NexusError(`Internal server error (HTTP ${httpStatus})`, McpErrorCode.InternalError, httpStatus, undefined, { retryable: true });
|
|
240
|
+
default:
|
|
241
|
+
// Catch-all for unexpected non-2xx codes not in the table.
|
|
242
|
+
return new NexusError(`Unexpected HTTP error (status=${httpStatus})`, McpErrorCode.InternalError, httpStatus, undefined, { retryable: false });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @nexusm/mcp-server — MCP server entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes 4 MVP tools (nexus.context_retrieve, nexus.memory_search,
|
|
6
|
+
* nexus.memory_create, nexus.memory_feedback) over the Model Context
|
|
7
|
+
* Protocol. Schemas are locked in
|
|
8
|
+
* `openspec/changes/us-037-mcp-server-exposure/proposal.md` §"R2 工具 Schema 锁定".
|
|
9
|
+
*
|
|
10
|
+
* Transport selection (proposal §M-14 dual transport):
|
|
11
|
+
* - Default: stdio (StdioServerTransport)
|
|
12
|
+
* - Optional: Streamable HTTP via env var `NEXUS_MCP_TRANSPORT=http`
|
|
13
|
+
*
|
|
14
|
+
* SDK package note: package.json depends on `@modelcontextprotocol/sdk`
|
|
15
|
+
* ^1.29 (v1 single package). The proposal §52 mentions a future v2 split
|
|
16
|
+
* (`@modelcontextprotocol/server` + `@modelcontextprotocol/node`); when the
|
|
17
|
+
* SDK migrates, update the import subpaths below — schema definitions and
|
|
18
|
+
* handler shapes are insulated in `src/tools/`.
|
|
19
|
+
*
|
|
20
|
+
* Logging discipline: stdio transport multiplexes JSON-RPC over stdin/stdout,
|
|
21
|
+
* so ALL diagnostic output must go to stderr (`console.error`). Never write
|
|
22
|
+
* to stdout outside the transport.
|
|
23
|
+
*
|
|
24
|
+
* Wave 1 (TASK-003): tool handlers return NOT_IMPLEMENTED. Wave 1+ (TASK-007..010)
|
|
25
|
+
* wires each handler to nexus-sdk-js. Auth (TASK-004) lives in src/auth.ts.
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @nexusm/mcp-server — MCP server entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes 4 MVP tools (nexus.context_retrieve, nexus.memory_search,
|
|
6
|
+
* nexus.memory_create, nexus.memory_feedback) over the Model Context
|
|
7
|
+
* Protocol. Schemas are locked in
|
|
8
|
+
* `openspec/changes/us-037-mcp-server-exposure/proposal.md` §"R2 工具 Schema 锁定".
|
|
9
|
+
*
|
|
10
|
+
* Transport selection (proposal §M-14 dual transport):
|
|
11
|
+
* - Default: stdio (StdioServerTransport)
|
|
12
|
+
* - Optional: Streamable HTTP via env var `NEXUS_MCP_TRANSPORT=http`
|
|
13
|
+
*
|
|
14
|
+
* SDK package note: package.json depends on `@modelcontextprotocol/sdk`
|
|
15
|
+
* ^1.29 (v1 single package). The proposal §52 mentions a future v2 split
|
|
16
|
+
* (`@modelcontextprotocol/server` + `@modelcontextprotocol/node`); when the
|
|
17
|
+
* SDK migrates, update the import subpaths below — schema definitions and
|
|
18
|
+
* handler shapes are insulated in `src/tools/`.
|
|
19
|
+
*
|
|
20
|
+
* Logging discipline: stdio transport multiplexes JSON-RPC over stdin/stdout,
|
|
21
|
+
* so ALL diagnostic output must go to stderr (`console.error`). Never write
|
|
22
|
+
* to stdout outside the transport.
|
|
23
|
+
*
|
|
24
|
+
* Wave 1 (TASK-003): tool handlers return NOT_IMPLEMENTED. Wave 1+ (TASK-007..010)
|
|
25
|
+
* wires each handler to nexus-sdk-js. Auth (TASK-004) lives in src/auth.ts.
|
|
26
|
+
*/
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
29
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
30
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
31
|
+
import { tools, toolsByName } from './tools/index.js';
|
|
32
|
+
import { startMetricsServer, emitToolCall, emitToolDuration, emitToolsList } from './metrics.js';
|
|
33
|
+
/**
|
|
34
|
+
* Extract the calling MCP client identifier from a request, with fallback.
|
|
35
|
+
*
|
|
36
|
+
* MCP `request._meta.clientInfo.name` is the standard JSON-RPC carrier when
|
|
37
|
+
* the client cooperates (Claude Code, Cursor, mcp-cli all set it). When the
|
|
38
|
+
* field is missing, fall back to env var `NEXUS_MCP_CLIENT_NAME` (set by
|
|
39
|
+
* launchers that wrap nexusm-mcp-server) and finally `unknown`. Cardinality
|
|
40
|
+
* is guarded inside `metrics.ts` via a whitelist (R2 ai D-5).
|
|
41
|
+
*/
|
|
42
|
+
function extractClient(request) {
|
|
43
|
+
const fromMeta = request._meta?.clientInfo?.name;
|
|
44
|
+
if (typeof fromMeta === 'string' && fromMeta.length > 0) {
|
|
45
|
+
return fromMeta;
|
|
46
|
+
}
|
|
47
|
+
const fromEnv = process.env.NEXUS_MCP_CLIENT_NAME;
|
|
48
|
+
return fromEnv && fromEnv.length > 0 ? fromEnv : 'unknown';
|
|
49
|
+
}
|
|
50
|
+
// Read version from package.json without `import assert` (Node 18+ compat).
|
|
51
|
+
const require = createRequire(import.meta.url);
|
|
52
|
+
const pkg = require('../package.json');
|
|
53
|
+
const SERVER_NAME = 'nexus';
|
|
54
|
+
const SERVER_VERSION = pkg.version;
|
|
55
|
+
function createServer() {
|
|
56
|
+
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
57
|
+
// tools/list — return the locked input/output schemas verbatim.
|
|
58
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
59
|
+
emitToolsList(extractClient(request));
|
|
60
|
+
return {
|
|
61
|
+
tools: tools.map((t) => ({
|
|
62
|
+
name: t.name,
|
|
63
|
+
description: t.description,
|
|
64
|
+
inputSchema: t.inputSchema,
|
|
65
|
+
outputSchema: t.outputSchema,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
// tools/call — dispatch to handler by tool name. Unknown tool names surface
|
|
70
|
+
// as protocol-level errors (per CallToolResult spec: "errors in _finding_
|
|
71
|
+
// the tool ... should be reported as an MCP error response").
|
|
72
|
+
// Wraps emitToolCall + emitToolDuration around the handler so metrics fire
|
|
73
|
+
// on both success + failure (R1 mid_audit C-2 + tech-lead Important #1).
|
|
74
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
75
|
+
const name = request.params.name;
|
|
76
|
+
const client = extractClient(request);
|
|
77
|
+
const tool = toolsByName.get(name);
|
|
78
|
+
if (!tool) {
|
|
79
|
+
emitToolCall(name, 'unknown_tool', client);
|
|
80
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
const args = (request.params.arguments ?? {});
|
|
83
|
+
const startNs = process.hrtime.bigint();
|
|
84
|
+
try {
|
|
85
|
+
const result = await tool.handler(args);
|
|
86
|
+
emitToolCall(name, 'success', client);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
emitToolCall(name, 'error', client);
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
const durNs = process.hrtime.bigint() - startNs;
|
|
95
|
+
emitToolDuration(name, Number(durNs) / 1e9);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return server;
|
|
99
|
+
}
|
|
100
|
+
async function startStdio(server) {
|
|
101
|
+
const transport = new StdioServerTransport();
|
|
102
|
+
await server.connect(transport);
|
|
103
|
+
console.error(`nexusm-mcp-server ${SERVER_VERSION} listening on stdio`);
|
|
104
|
+
}
|
|
105
|
+
async function startHttp(server) {
|
|
106
|
+
// Lazy import so stdio installs aren't forced to bundle the HTTP transport.
|
|
107
|
+
const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
108
|
+
const { createServer: createHttpServer } = await import('node:http');
|
|
109
|
+
const port = Number.parseInt(process.env.NEXUS_MCP_HTTP_PORT ?? '3000', 10);
|
|
110
|
+
// TODO(wave-2): per-request `server.connect()` + per-request transport
|
|
111
|
+
// is scaffold-only. Production HTTP transport needs session-keyed Server
|
|
112
|
+
// lifecycle per MCP spec (see TASK-014 integration tests + TASK-018
|
|
113
|
+
// middleware in detailed-tasks.yaml).
|
|
114
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
115
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
116
|
+
await server.connect(transport);
|
|
117
|
+
await transport.handleRequest(req, res);
|
|
118
|
+
});
|
|
119
|
+
httpServer.listen(port, () => {
|
|
120
|
+
console.error(`nexusm-mcp-server ${SERVER_VERSION} listening on http://0.0.0.0:${port}/`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function main() {
|
|
124
|
+
const server = createServer();
|
|
125
|
+
const transportMode = (process.env.NEXUS_MCP_TRANSPORT ?? 'stdio').toLowerCase();
|
|
126
|
+
// Start Prometheus metrics server on separate port (TASK-014, R2 m-6).
|
|
127
|
+
// Coexists with stdio transport — different fd / no stdin/stdout contention.
|
|
128
|
+
await startMetricsServer();
|
|
129
|
+
if (transportMode === 'http') {
|
|
130
|
+
await startHttp(server);
|
|
131
|
+
}
|
|
132
|
+
else if (transportMode === 'stdio') {
|
|
133
|
+
await startStdio(server);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.error(`Unknown NEXUS_MCP_TRANSPORT="${transportMode}" (expected "stdio" or "http")`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
main().catch((err) => {
|
|
141
|
+
// Never let an unhandled error leak credentials. stderr only.
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
console.error(`nexusm-mcp-server fatal: ${msg}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus metrics for the Nexus MCP server (US-037 Wave 2 TASK-014).
|
|
3
|
+
*
|
|
4
|
+
* Layer
|
|
5
|
+
* =====
|
|
6
|
+
* This file measures the **MCP protocol dispatch layer** — every `tools/call`
|
|
7
|
+
* and `tools/list` JSON-RPC request the MCP server handles. It is NOT the
|
|
8
|
+
* same as the Python-side `src/nexus/observability/metrics/mcp.py` which
|
|
9
|
+
* measures the **backend REST attribution layer** (post-MCP-dispatch).
|
|
10
|
+
*
|
|
11
|
+
* Both metric families share the `nexus_mcp_*` prefix but observe different
|
|
12
|
+
* events at different layers, so:
|
|
13
|
+
* - TS metric names use `nexus_mcp_tool_*` (this file)
|
|
14
|
+
* - Python metric names use `nexus_mcp_backend_*` (the backend mirror)
|
|
15
|
+
* - Alert rules and Grafana panels must NOT sum across both — they would
|
|
16
|
+
* double-count a single user action
|
|
17
|
+
* - See ADR-001 + Wave 2 mid_audit tech-lead Important #2 (2026-05-22)
|
|
18
|
+
*
|
|
19
|
+
* Design notes
|
|
20
|
+
* ============
|
|
21
|
+
* - Spins up an **independent** HTTP listener on `NEXUS_METRICS_PORT` (default
|
|
22
|
+
* 9090) so the Prometheus scrape endpoint never touches stdin/stdout — the
|
|
23
|
+
* stdio MCP transport monopolises those streams.
|
|
24
|
+
* - Uses `prom-client` for registry management; all metric instances are
|
|
25
|
+
* module-level singletons (CollectorRegistry deduplication).
|
|
26
|
+
* - Cardinality guard (R2 ai D-5): the `client` label is limited to a fixed
|
|
27
|
+
* allowlist; anything outside the list is coerced to `'unknown'` and a
|
|
28
|
+
* separate debug counter tracks the raw string so operators can promote a
|
|
29
|
+
* client to the allowlist without restarting the server.
|
|
30
|
+
*
|
|
31
|
+
* Startup integration
|
|
32
|
+
* ===================
|
|
33
|
+
* Call `startMetricsServer()` once from `src/index.ts` main() — it is a
|
|
34
|
+
* fire-and-forget async function that opens the HTTP port and logs to stderr.
|
|
35
|
+
* (Parent session must wire this call; see TASK-014 note.)
|
|
36
|
+
*
|
|
37
|
+
* prom-client npm dep note
|
|
38
|
+
* ========================
|
|
39
|
+
* Add `"prom-client": "^15.1.0"` to `dependencies` in `package.json`.
|
|
40
|
+
* This subagent does not modify package.json per task constraints.
|
|
41
|
+
*/
|
|
42
|
+
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
|
|
43
|
+
export declare const registry: Registry<"text/plain; version=0.0.4; charset=utf-8">;
|
|
44
|
+
/**
|
|
45
|
+
* nexus_mcp_tool_calls_total — counts every tools/call dispatch.
|
|
46
|
+
*
|
|
47
|
+
* Labels:
|
|
48
|
+
* tool — MCP tool name (e.g. `nexus.memory_search`)
|
|
49
|
+
* status — `'success'` | `'error'` | `'not_implemented'`
|
|
50
|
+
* client — normalised client name from KNOWN_CLIENTS (or `'unknown'`)
|
|
51
|
+
*/
|
|
52
|
+
export declare const MCP_TOOL_CALLS_TOTAL: Counter<"status" | "tool" | "client">;
|
|
53
|
+
/**
|
|
54
|
+
* nexus_mcp_tool_duration_seconds — latency histogram per tool.
|
|
55
|
+
*
|
|
56
|
+
* Buckets cover sub-millisecond to 10 s to capture both local (fast) and
|
|
57
|
+
* remote Nexus API (network-bound) call distributions.
|
|
58
|
+
*
|
|
59
|
+
* Labels:
|
|
60
|
+
* tool — MCP tool name
|
|
61
|
+
*/
|
|
62
|
+
export declare const MCP_TOOL_DURATION_SECONDS: Histogram<"tool">;
|
|
63
|
+
/**
|
|
64
|
+
* nexus_mcp_tools_list_calls_total — counts every tools/list request.
|
|
65
|
+
*
|
|
66
|
+
* Labels:
|
|
67
|
+
* client — normalised client name
|
|
68
|
+
*/
|
|
69
|
+
export declare const MCP_TOOLS_LIST_CALLS_TOTAL: Counter<"client">;
|
|
70
|
+
/**
|
|
71
|
+
* nexus_mcp_tool_description_version — Info-style gauge exposing the schema hash.
|
|
72
|
+
*
|
|
73
|
+
* Set to 1.0 with a `hash` label containing the first 8 characters of the
|
|
74
|
+
* SHA-256 of the concatenated tool description strings. Alerts fire when
|
|
75
|
+
* the hash drifts between replicas or between server restarts.
|
|
76
|
+
*
|
|
77
|
+
* Labels:
|
|
78
|
+
* hash — 8-char hex prefix of SHA-256(all tool descriptions)
|
|
79
|
+
*/
|
|
80
|
+
export declare const MCP_TOOL_DESCRIPTION_VERSION: Gauge<"hash">;
|
|
81
|
+
/**
|
|
82
|
+
* nexus_mcp_unknown_client_total — debug counter for cardinality guard.
|
|
83
|
+
*
|
|
84
|
+
* Incremented when a raw client name is not in KNOWN_CLIENTS. The `raw`
|
|
85
|
+
* label carries the original string so operators can identify clients to
|
|
86
|
+
* add to the allowlist.
|
|
87
|
+
*
|
|
88
|
+
* Labels:
|
|
89
|
+
* raw — the raw client identifier as received (verbatim)
|
|
90
|
+
*/
|
|
91
|
+
export declare const MCP_UNKNOWN_CLIENT_TOTAL: Counter<"raw">;
|
|
92
|
+
/**
|
|
93
|
+
* Record a tool/call dispatch.
|
|
94
|
+
*
|
|
95
|
+
* @param tool - MCP tool name (e.g. `'nexus.memory_search'`)
|
|
96
|
+
* @param status - outcome string (`'success'`, `'error'`, `'not_implemented'`)
|
|
97
|
+
* @param client - raw client identifier; normalised internally against the allowlist
|
|
98
|
+
*/
|
|
99
|
+
export declare function emitToolCall(tool: string, status: string, client: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Record a tools/list request.
|
|
102
|
+
*
|
|
103
|
+
* @param client - raw client identifier; normalised internally against the allowlist
|
|
104
|
+
*/
|
|
105
|
+
export declare function emitToolsList(client: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Record an observation in the tool duration histogram.
|
|
108
|
+
*
|
|
109
|
+
* Call this with `tool` and the elapsed time in seconds. Designed so callers
|
|
110
|
+
* wrap their handler with `Date.now()` before/after and pass
|
|
111
|
+
* `(end - start) / 1000`.
|
|
112
|
+
*
|
|
113
|
+
* @param tool - MCP tool name
|
|
114
|
+
* @param durationSec - elapsed time in seconds
|
|
115
|
+
*/
|
|
116
|
+
export declare function emitToolDuration(tool: string, durationSec: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Increment the debug counter for an unknown client.
|
|
119
|
+
*
|
|
120
|
+
* Called internally by `emitToolCall` and `emitToolsList`; exposed so callers
|
|
121
|
+
* can also emit directly if they detect the anomaly before dispatching.
|
|
122
|
+
*
|
|
123
|
+
* @param rawName - raw client string that was not in the allowlist
|
|
124
|
+
*/
|
|
125
|
+
export declare function emitUnknownClient(rawName: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Register the tool-description schema hash in the Info gauge.
|
|
128
|
+
*
|
|
129
|
+
* Set this once during startup after the tool registry is built. Pass the
|
|
130
|
+
* first 8 hex characters of SHA-256(concatenated tool descriptions) as `hash`.
|
|
131
|
+
*
|
|
132
|
+
* @param hash - 8-char hex prefix of the schema hash
|
|
133
|
+
*/
|
|
134
|
+
export declare function setDescriptionHash(hash: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Start the Prometheus metrics scrape endpoint.
|
|
137
|
+
*
|
|
138
|
+
* Opens an HTTP server on `process.env.NEXUS_METRICS_PORT ?? 9090`. The
|
|
139
|
+
* server only handles `GET /metrics`; all other paths return 404.
|
|
140
|
+
*
|
|
141
|
+
* This function must be called from `src/index.ts` main() (parent session
|
|
142
|
+
* wires the call). It is intentionally fire-and-forget — it does not compete
|
|
143
|
+
* with the MCP transport for stdin/stdout.
|
|
144
|
+
*/
|
|
145
|
+
export declare function startMetricsServer(): Promise<void>;
|
|
146
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAyB,MAAM,aAAa,CAAC;AAQzF,eAAO,MAAM,QAAQ,sDAAiB,CAAC;AAuBvC;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,uCAK/B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,mBAMpC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,mBAKrC,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,eAKvC,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,gBAKnC,CAAC;AAMH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAM/E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAExE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAErD;AAMD;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA2BxD"}
|