@casys/mcp-server 0.8.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/mod.ts +161 -0
- package/package.json +32 -0
- package/src/auth/config.ts +229 -0
- package/src/auth/jwt-provider.ts +175 -0
- package/src/auth/middleware.ts +170 -0
- package/src/auth/mod.ts +44 -0
- package/src/auth/presets.ts +129 -0
- package/src/auth/provider.ts +47 -0
- package/src/auth/scope-middleware.ts +59 -0
- package/src/auth/types.ts +69 -0
- package/src/concurrency/rate-limiter.ts +190 -0
- package/src/concurrency/request-queue.ts +140 -0
- package/src/concurrent-server.ts +1899 -0
- package/src/middleware/backpressure.ts +36 -0
- package/src/middleware/mod.ts +21 -0
- package/src/middleware/rate-limit.ts +45 -0
- package/src/middleware/runner.ts +63 -0
- package/src/middleware/types.ts +60 -0
- package/src/middleware/validation.ts +28 -0
- package/src/observability/metrics.ts +378 -0
- package/src/observability/mod.ts +20 -0
- package/src/observability/otel.ts +109 -0
- package/src/runtime/runtime.ts +220 -0
- package/src/runtime/types.ts +90 -0
- package/src/sampling/sampling-bridge.ts +191 -0
- package/src/security/channel-hmac.ts +140 -0
- package/src/security/csp.ts +87 -0
- package/src/security/message-signer.ts +223 -0
- package/src/types.ts +478 -0
- package/src/validation/schema-validator.ts +238 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backpressure middleware.
|
|
3
|
+
*
|
|
4
|
+
* Controls concurrent execution using RequestQueue.
|
|
5
|
+
* Acquires a slot before calling next(), releases after.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/middleware/backpressure
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RequestQueue } from "../concurrency/request-queue.js";
|
|
11
|
+
import type { Middleware } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a backpressure middleware.
|
|
15
|
+
*
|
|
16
|
+
* Wraps the downstream pipeline in acquire/release:
|
|
17
|
+
* ```
|
|
18
|
+
* acquire() → next() → release()
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The slot is always released, even if an error occurs.
|
|
22
|
+
*
|
|
23
|
+
* @param queue - RequestQueue instance with configured concurrency limits
|
|
24
|
+
*/
|
|
25
|
+
export function createBackpressureMiddleware(
|
|
26
|
+
queue: RequestQueue,
|
|
27
|
+
): Middleware {
|
|
28
|
+
return async (_ctx, next) => {
|
|
29
|
+
await queue.acquire();
|
|
30
|
+
try {
|
|
31
|
+
return await next();
|
|
32
|
+
} finally {
|
|
33
|
+
queue.release();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware module for ConcurrentMCPServer.
|
|
3
|
+
*
|
|
4
|
+
* @module lib/server/middleware
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
Middleware,
|
|
10
|
+
MiddlewareContext,
|
|
11
|
+
MiddlewareResult,
|
|
12
|
+
NextFunction,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
// Runner
|
|
16
|
+
export { createMiddlewareRunner } from "./runner.js";
|
|
17
|
+
|
|
18
|
+
// Built-in middlewares
|
|
19
|
+
export { createRateLimitMiddleware } from "./rate-limit.js";
|
|
20
|
+
export { createValidationMiddleware } from "./validation.js";
|
|
21
|
+
export { createBackpressureMiddleware } from "./backpressure.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiting middleware.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ConcurrentMCPServer's inline rate limit logic.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/server/middleware/rate-limit
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RateLimiter } from "../concurrency/rate-limiter.js";
|
|
10
|
+
import type { RateLimitOptions } from "../types.js";
|
|
11
|
+
import type { Middleware } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a rate limiting middleware.
|
|
15
|
+
*
|
|
16
|
+
* Behavior depends on `options.onLimitExceeded`:
|
|
17
|
+
* - `'reject'`: throws immediately when limit is exceeded
|
|
18
|
+
* - `'wait'` (default): waits with backoff until a slot is available
|
|
19
|
+
*
|
|
20
|
+
* @param limiter - RateLimiter instance
|
|
21
|
+
* @param options - Rate limit configuration
|
|
22
|
+
*/
|
|
23
|
+
export function createRateLimitMiddleware(
|
|
24
|
+
limiter: RateLimiter,
|
|
25
|
+
options: RateLimitOptions,
|
|
26
|
+
): Middleware {
|
|
27
|
+
return async (ctx, next) => {
|
|
28
|
+
const key =
|
|
29
|
+
options.keyExtractor?.({ toolName: ctx.toolName, args: ctx.args }) ??
|
|
30
|
+
"default";
|
|
31
|
+
|
|
32
|
+
if (options.onLimitExceeded === "reject") {
|
|
33
|
+
if (!limiter.checkLimit(key)) {
|
|
34
|
+
const waitTime = limiter.getTimeUntilSlot(key);
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Rate limit exceeded. Retry after ${Math.ceil(waitTime / 1000)}s`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
await limiter.waitForSlot(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware pipeline runner.
|
|
3
|
+
*
|
|
4
|
+
* Composes an array of middlewares into a single callable function
|
|
5
|
+
* using the onion model: each middleware wraps the next.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/middleware/runner
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Middleware,
|
|
12
|
+
MiddlewareContext,
|
|
13
|
+
MiddlewareResult,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a middleware runner that composes middlewares + a final handler.
|
|
18
|
+
*
|
|
19
|
+
* Execution order (onion model):
|
|
20
|
+
* ```
|
|
21
|
+
* m1-before → m2-before → handler → m2-after → m1-after
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param middlewares - Array of middleware functions to execute in order
|
|
25
|
+
* @param handler - Final handler (the tool execution)
|
|
26
|
+
* @returns A function that runs the full pipeline for a given context
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const run = createMiddlewareRunner(
|
|
31
|
+
* [rateLimitMiddleware, validationMiddleware],
|
|
32
|
+
* async (ctx) => toolHandler(ctx.args),
|
|
33
|
+
* );
|
|
34
|
+
* const result = await run({ toolName: "my_tool", args: { x: 1 } });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function createMiddlewareRunner(
|
|
38
|
+
middlewares: Middleware[],
|
|
39
|
+
handler: (ctx: MiddlewareContext) => Promise<MiddlewareResult>,
|
|
40
|
+
): (ctx: MiddlewareContext) => Promise<MiddlewareResult> {
|
|
41
|
+
return (ctx: MiddlewareContext) => {
|
|
42
|
+
let index = 0;
|
|
43
|
+
let handlerCalled = false;
|
|
44
|
+
|
|
45
|
+
// deno-lint-ignore require-await
|
|
46
|
+
const next = async (): Promise<MiddlewareResult> => {
|
|
47
|
+
if (index < middlewares.length) {
|
|
48
|
+
const middleware = middlewares[index++];
|
|
49
|
+
return middleware(ctx, next);
|
|
50
|
+
}
|
|
51
|
+
if (handlerCalled) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"[MiddlewareRunner] next() called after pipeline already completed. " +
|
|
54
|
+
"A middleware may be calling next() multiple times.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
handlerCalled = true;
|
|
58
|
+
return handler(ctx);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return next();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware pipeline types for ConcurrentMCPServer.
|
|
3
|
+
*
|
|
4
|
+
* Provides an onion-model middleware system (similar to Koa/Hono)
|
|
5
|
+
* where each middleware wraps the next, enabling before/after logic.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/middleware/types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context passed through the middleware pipeline.
|
|
12
|
+
* Each middleware can read and enrich the context.
|
|
13
|
+
*/
|
|
14
|
+
export interface MiddlewareContext {
|
|
15
|
+
/** Name of the tool being called */
|
|
16
|
+
toolName: string;
|
|
17
|
+
|
|
18
|
+
/** Tool arguments */
|
|
19
|
+
args: Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
/** HTTP request (only present for HTTP transport, undefined for STDIO) */
|
|
22
|
+
request?: Request;
|
|
23
|
+
|
|
24
|
+
/** Session ID (only present for HTTP transport) */
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
|
|
27
|
+
/** Extensible by middlewares (e.g., authInfo added by auth middleware) */
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result returned by a middleware or the final handler.
|
|
33
|
+
*/
|
|
34
|
+
export type MiddlewareResult = unknown;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Function to invoke the next middleware in the chain.
|
|
38
|
+
*/
|
|
39
|
+
export type NextFunction = () => Promise<MiddlewareResult>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A middleware function.
|
|
43
|
+
*
|
|
44
|
+
* Receives the context and a `next()` function to call the next middleware.
|
|
45
|
+
* Can short-circuit the pipeline by not calling `next()`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const loggingMiddleware: Middleware = async (ctx, next) => {
|
|
50
|
+
* console.log(`Before: ${ctx.toolName}`);
|
|
51
|
+
* const result = await next();
|
|
52
|
+
* console.log(`After: ${ctx.toolName}`);
|
|
53
|
+
* return result;
|
|
54
|
+
* };
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export type Middleware = (
|
|
58
|
+
ctx: MiddlewareContext,
|
|
59
|
+
next: NextFunction,
|
|
60
|
+
) => Promise<MiddlewareResult>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema validation middleware.
|
|
3
|
+
*
|
|
4
|
+
* Validates tool arguments against JSON Schema before execution.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/server/middleware/validation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SchemaValidator } from "../validation/schema-validator.js";
|
|
10
|
+
import type { Middleware } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a schema validation middleware.
|
|
14
|
+
*
|
|
15
|
+
* Validates `ctx.args` against the registered schema for `ctx.toolName`.
|
|
16
|
+
* Throws with a descriptive error if validation fails.
|
|
17
|
+
*
|
|
18
|
+
* @param validator - SchemaValidator instance with pre-registered schemas
|
|
19
|
+
*/
|
|
20
|
+
export function createValidationMiddleware(
|
|
21
|
+
validator: SchemaValidator,
|
|
22
|
+
): Middleware {
|
|
23
|
+
// deno-lint-ignore require-await
|
|
24
|
+
return async (ctx, next) => {
|
|
25
|
+
validator.validateOrThrow(ctx.toolName, ctx.args);
|
|
26
|
+
return next();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Metrics Collector for @casys/mcp-server
|
|
3
|
+
*
|
|
4
|
+
* In-memory counters, histograms, and gauges with Prometheus text format export.
|
|
5
|
+
* Designed to be embedded in ConcurrentMCPServer — no external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/observability/metrics
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Histogram bucket
|
|
12
|
+
*/
|
|
13
|
+
interface HistogramBucket {
|
|
14
|
+
le: number;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Latency histogram with cumulative buckets
|
|
20
|
+
*/
|
|
21
|
+
interface Histogram {
|
|
22
|
+
buckets: HistogramBucket[];
|
|
23
|
+
sum: number;
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default histogram buckets (milliseconds)
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_BUCKETS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
|
|
31
|
+
|
|
32
|
+
function createHistogram(buckets: number[] = DEFAULT_BUCKETS): Histogram {
|
|
33
|
+
return {
|
|
34
|
+
buckets: buckets.map((le) => ({ le, count: 0 })),
|
|
35
|
+
sum: 0,
|
|
36
|
+
count: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function observeHistogram(histogram: Histogram, value: number): void {
|
|
41
|
+
histogram.sum += value;
|
|
42
|
+
histogram.count++;
|
|
43
|
+
for (const bucket of histogram.buckets) {
|
|
44
|
+
if (value <= bucket.le) {
|
|
45
|
+
bucket.count++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Metrics snapshot returned by getMetrics()
|
|
52
|
+
*/
|
|
53
|
+
export interface ServerMetricsSnapshot {
|
|
54
|
+
counters: {
|
|
55
|
+
tool_calls_total: number;
|
|
56
|
+
tool_calls_success: number;
|
|
57
|
+
tool_calls_failed: number;
|
|
58
|
+
requests_rate_limited: number;
|
|
59
|
+
requests_rejected_backpressure: number;
|
|
60
|
+
auth_success: number;
|
|
61
|
+
auth_failed: number;
|
|
62
|
+
auth_cache_hits: number;
|
|
63
|
+
sessions_created: number;
|
|
64
|
+
sessions_expired: number;
|
|
65
|
+
};
|
|
66
|
+
histograms: {
|
|
67
|
+
tool_call_duration_ms: Histogram;
|
|
68
|
+
};
|
|
69
|
+
gauges: {
|
|
70
|
+
active_requests: number;
|
|
71
|
+
queued_requests: number;
|
|
72
|
+
active_sessions: number;
|
|
73
|
+
sse_clients: number;
|
|
74
|
+
rate_limiter_keys: number;
|
|
75
|
+
};
|
|
76
|
+
collected_at: number;
|
|
77
|
+
uptime_seconds: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tool call metrics by tool name
|
|
82
|
+
*/
|
|
83
|
+
interface PerToolMetrics {
|
|
84
|
+
calls: number;
|
|
85
|
+
success: number;
|
|
86
|
+
failed: number;
|
|
87
|
+
totalDurationMs: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Server metrics collector.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const metrics = new ServerMetrics();
|
|
96
|
+
* metrics.recordToolCall("my_tool", true, 42);
|
|
97
|
+
* console.log(metrics.toPrometheusFormat());
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export class ServerMetrics {
|
|
101
|
+
private startTime = Date.now();
|
|
102
|
+
|
|
103
|
+
// Counters
|
|
104
|
+
private toolCallsTotal = 0;
|
|
105
|
+
private toolCallsSuccess = 0;
|
|
106
|
+
private toolCallsFailed = 0;
|
|
107
|
+
private requestsRateLimited = 0;
|
|
108
|
+
private requestsRejectedBackpressure = 0;
|
|
109
|
+
private authSuccess = 0;
|
|
110
|
+
private authFailed = 0;
|
|
111
|
+
private authCacheHits = 0;
|
|
112
|
+
private sessionsCreated = 0;
|
|
113
|
+
private sessionsExpired = 0;
|
|
114
|
+
|
|
115
|
+
// Histogram
|
|
116
|
+
private toolCallDuration = createHistogram();
|
|
117
|
+
|
|
118
|
+
// Per-tool breakdown
|
|
119
|
+
private perTool = new Map<string, PerToolMetrics>();
|
|
120
|
+
|
|
121
|
+
// Gauges (set externally via setGauge)
|
|
122
|
+
private activeRequests = 0;
|
|
123
|
+
private queuedRequests = 0;
|
|
124
|
+
private activeSessions = 0;
|
|
125
|
+
private sseClients = 0;
|
|
126
|
+
private rateLimiterKeys = 0;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Record a completed tool call
|
|
130
|
+
*/
|
|
131
|
+
recordToolCall(toolName: string, success: boolean, durationMs: number): void {
|
|
132
|
+
this.toolCallsTotal++;
|
|
133
|
+
if (success) {
|
|
134
|
+
this.toolCallsSuccess++;
|
|
135
|
+
} else {
|
|
136
|
+
this.toolCallsFailed++;
|
|
137
|
+
}
|
|
138
|
+
observeHistogram(this.toolCallDuration, durationMs);
|
|
139
|
+
|
|
140
|
+
// Per-tool
|
|
141
|
+
let pt = this.perTool.get(toolName);
|
|
142
|
+
if (!pt) {
|
|
143
|
+
pt = { calls: 0, success: 0, failed: 0, totalDurationMs: 0 };
|
|
144
|
+
this.perTool.set(toolName, pt);
|
|
145
|
+
}
|
|
146
|
+
pt.calls++;
|
|
147
|
+
if (success) pt.success++;
|
|
148
|
+
else pt.failed++;
|
|
149
|
+
pt.totalDurationMs += durationMs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
recordRateLimited(): void {
|
|
153
|
+
this.requestsRateLimited++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
recordBackpressureRejected(): void {
|
|
157
|
+
this.requestsRejectedBackpressure++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
recordAuth(success: boolean): void {
|
|
161
|
+
if (success) this.authSuccess++;
|
|
162
|
+
else this.authFailed++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
recordAuthCacheHit(): void {
|
|
166
|
+
this.authCacheHits++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
recordSessionCreated(): void {
|
|
170
|
+
this.sessionsCreated++;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
recordSessionExpired(count: number): void {
|
|
174
|
+
this.sessionsExpired += count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update gauge values (called periodically or on-demand)
|
|
179
|
+
*/
|
|
180
|
+
setGauges(gauges: {
|
|
181
|
+
activeRequests?: number;
|
|
182
|
+
queuedRequests?: number;
|
|
183
|
+
activeSessions?: number;
|
|
184
|
+
sseClients?: number;
|
|
185
|
+
rateLimiterKeys?: number;
|
|
186
|
+
}): void {
|
|
187
|
+
if (gauges.activeRequests !== undefined) {
|
|
188
|
+
this.activeRequests = gauges.activeRequests;
|
|
189
|
+
}
|
|
190
|
+
if (gauges.queuedRequests !== undefined) {
|
|
191
|
+
this.queuedRequests = gauges.queuedRequests;
|
|
192
|
+
}
|
|
193
|
+
if (gauges.activeSessions !== undefined) {
|
|
194
|
+
this.activeSessions = gauges.activeSessions;
|
|
195
|
+
}
|
|
196
|
+
if (gauges.sseClients !== undefined) this.sseClients = gauges.sseClients;
|
|
197
|
+
if (gauges.rateLimiterKeys !== undefined) {
|
|
198
|
+
this.rateLimiterKeys = gauges.rateLimiterKeys;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get current metrics snapshot
|
|
204
|
+
*/
|
|
205
|
+
getSnapshot(): ServerMetricsSnapshot {
|
|
206
|
+
return {
|
|
207
|
+
counters: {
|
|
208
|
+
tool_calls_total: this.toolCallsTotal,
|
|
209
|
+
tool_calls_success: this.toolCallsSuccess,
|
|
210
|
+
tool_calls_failed: this.toolCallsFailed,
|
|
211
|
+
requests_rate_limited: this.requestsRateLimited,
|
|
212
|
+
requests_rejected_backpressure: this.requestsRejectedBackpressure,
|
|
213
|
+
auth_success: this.authSuccess,
|
|
214
|
+
auth_failed: this.authFailed,
|
|
215
|
+
auth_cache_hits: this.authCacheHits,
|
|
216
|
+
sessions_created: this.sessionsCreated,
|
|
217
|
+
sessions_expired: this.sessionsExpired,
|
|
218
|
+
},
|
|
219
|
+
histograms: {
|
|
220
|
+
tool_call_duration_ms: { ...this.toolCallDuration },
|
|
221
|
+
},
|
|
222
|
+
gauges: {
|
|
223
|
+
active_requests: this.activeRequests,
|
|
224
|
+
queued_requests: this.queuedRequests,
|
|
225
|
+
active_sessions: this.activeSessions,
|
|
226
|
+
sse_clients: this.sseClients,
|
|
227
|
+
rate_limiter_keys: this.rateLimiterKeys,
|
|
228
|
+
},
|
|
229
|
+
collected_at: Date.now(),
|
|
230
|
+
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Prometheus text format export
|
|
236
|
+
*/
|
|
237
|
+
toPrometheusFormat(prefix = "mcp_server"): string {
|
|
238
|
+
const m = this.getSnapshot();
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
|
|
241
|
+
// --- Counters ---
|
|
242
|
+
const counter = (name: string, help: string, value: number) => {
|
|
243
|
+
lines.push(`# HELP ${prefix}_${name} ${help}`);
|
|
244
|
+
lines.push(`# TYPE ${prefix}_${name} counter`);
|
|
245
|
+
lines.push(`${prefix}_${name} ${value}`);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
counter(
|
|
249
|
+
"tool_calls_total",
|
|
250
|
+
"Total tool calls",
|
|
251
|
+
m.counters.tool_calls_total,
|
|
252
|
+
);
|
|
253
|
+
counter(
|
|
254
|
+
"tool_calls_success_total",
|
|
255
|
+
"Successful tool calls",
|
|
256
|
+
m.counters.tool_calls_success,
|
|
257
|
+
);
|
|
258
|
+
counter(
|
|
259
|
+
"tool_calls_failed_total",
|
|
260
|
+
"Failed tool calls",
|
|
261
|
+
m.counters.tool_calls_failed,
|
|
262
|
+
);
|
|
263
|
+
counter(
|
|
264
|
+
"requests_rate_limited_total",
|
|
265
|
+
"Requests rejected by rate limiter",
|
|
266
|
+
m.counters.requests_rate_limited,
|
|
267
|
+
);
|
|
268
|
+
counter(
|
|
269
|
+
"requests_backpressure_total",
|
|
270
|
+
"Requests rejected by backpressure",
|
|
271
|
+
m.counters.requests_rejected_backpressure,
|
|
272
|
+
);
|
|
273
|
+
counter(
|
|
274
|
+
"auth_success_total",
|
|
275
|
+
"Successful auth verifications",
|
|
276
|
+
m.counters.auth_success,
|
|
277
|
+
);
|
|
278
|
+
counter(
|
|
279
|
+
"auth_failed_total",
|
|
280
|
+
"Failed auth verifications",
|
|
281
|
+
m.counters.auth_failed,
|
|
282
|
+
);
|
|
283
|
+
counter(
|
|
284
|
+
"auth_cache_hits_total",
|
|
285
|
+
"Auth token cache hits",
|
|
286
|
+
m.counters.auth_cache_hits,
|
|
287
|
+
);
|
|
288
|
+
counter(
|
|
289
|
+
"sessions_created_total",
|
|
290
|
+
"Sessions created",
|
|
291
|
+
m.counters.sessions_created,
|
|
292
|
+
);
|
|
293
|
+
counter(
|
|
294
|
+
"sessions_expired_total",
|
|
295
|
+
"Sessions expired by cleanup",
|
|
296
|
+
m.counters.sessions_expired,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// --- Per-tool counters ---
|
|
300
|
+
lines.push(`# HELP ${prefix}_tool_calls_by_name Tool calls by tool name`);
|
|
301
|
+
lines.push(`# TYPE ${prefix}_tool_calls_by_name counter`);
|
|
302
|
+
for (const [name, pt] of this.perTool) {
|
|
303
|
+
lines.push(
|
|
304
|
+
`${prefix}_tool_calls_by_name{tool="${name}",status="success"} ${pt.success}`,
|
|
305
|
+
);
|
|
306
|
+
lines.push(
|
|
307
|
+
`${prefix}_tool_calls_by_name{tool="${name}",status="failed"} ${pt.failed}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Histogram ---
|
|
312
|
+
const h = m.histograms.tool_call_duration_ms;
|
|
313
|
+
lines.push(
|
|
314
|
+
`# HELP ${prefix}_tool_call_duration_ms Tool call duration in milliseconds`,
|
|
315
|
+
);
|
|
316
|
+
lines.push(`# TYPE ${prefix}_tool_call_duration_ms histogram`);
|
|
317
|
+
for (const bucket of h.buckets) {
|
|
318
|
+
lines.push(
|
|
319
|
+
`${prefix}_tool_call_duration_ms_bucket{le="${bucket.le}"} ${bucket.count}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
lines.push(`${prefix}_tool_call_duration_ms_bucket{le="+Inf"} ${h.count}`);
|
|
323
|
+
lines.push(`${prefix}_tool_call_duration_ms_sum ${h.sum}`);
|
|
324
|
+
lines.push(`${prefix}_tool_call_duration_ms_count ${h.count}`);
|
|
325
|
+
|
|
326
|
+
// --- Gauges ---
|
|
327
|
+
const gauge = (name: string, help: string, value: number) => {
|
|
328
|
+
lines.push(`# HELP ${prefix}_${name} ${help}`);
|
|
329
|
+
lines.push(`# TYPE ${prefix}_${name} gauge`);
|
|
330
|
+
lines.push(`${prefix}_${name} ${value}`);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
gauge(
|
|
334
|
+
"active_requests",
|
|
335
|
+
"Currently executing requests",
|
|
336
|
+
m.gauges.active_requests,
|
|
337
|
+
);
|
|
338
|
+
gauge(
|
|
339
|
+
"queued_requests",
|
|
340
|
+
"Requests waiting in queue",
|
|
341
|
+
m.gauges.queued_requests,
|
|
342
|
+
);
|
|
343
|
+
gauge("active_sessions", "Active HTTP sessions", m.gauges.active_sessions);
|
|
344
|
+
gauge("sse_clients", "Connected SSE clients", m.gauges.sse_clients);
|
|
345
|
+
gauge(
|
|
346
|
+
"rate_limiter_keys",
|
|
347
|
+
"Tracked rate limiter keys",
|
|
348
|
+
m.gauges.rate_limiter_keys,
|
|
349
|
+
);
|
|
350
|
+
gauge("uptime_seconds", "Server uptime in seconds", m.uptime_seconds);
|
|
351
|
+
|
|
352
|
+
return lines.join("\n") + "\n";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Reset all metrics
|
|
357
|
+
*/
|
|
358
|
+
reset(): void {
|
|
359
|
+
this.toolCallsTotal = 0;
|
|
360
|
+
this.toolCallsSuccess = 0;
|
|
361
|
+
this.toolCallsFailed = 0;
|
|
362
|
+
this.requestsRateLimited = 0;
|
|
363
|
+
this.requestsRejectedBackpressure = 0;
|
|
364
|
+
this.authSuccess = 0;
|
|
365
|
+
this.authFailed = 0;
|
|
366
|
+
this.authCacheHits = 0;
|
|
367
|
+
this.sessionsCreated = 0;
|
|
368
|
+
this.sessionsExpired = 0;
|
|
369
|
+
this.toolCallDuration = createHistogram();
|
|
370
|
+
this.perTool.clear();
|
|
371
|
+
this.activeRequests = 0;
|
|
372
|
+
this.queuedRequests = 0;
|
|
373
|
+
this.activeSessions = 0;
|
|
374
|
+
this.sseClients = 0;
|
|
375
|
+
this.rateLimiterKeys = 0;
|
|
376
|
+
this.startTime = Date.now();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability module for @casys/mcp-server
|
|
3
|
+
*
|
|
4
|
+
* - OTel tracing (spans on tool calls, auth events)
|
|
5
|
+
* - Metrics collection (counters, histograms, gauges)
|
|
6
|
+
* - Prometheus text format export
|
|
7
|
+
*
|
|
8
|
+
* @module lib/server/observability
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
endToolCallSpan,
|
|
13
|
+
getServerTracer,
|
|
14
|
+
isOtelEnabled,
|
|
15
|
+
recordAuthEvent,
|
|
16
|
+
startToolCallSpan,
|
|
17
|
+
type ToolCallSpanAttributes,
|
|
18
|
+
} from "./otel.js";
|
|
19
|
+
|
|
20
|
+
export { ServerMetrics, type ServerMetricsSnapshot } from "./metrics.js";
|