@vesta-analytics/sdk 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 +202 -0
- package/README.md +88 -0
- package/dist/adapters/base.d.ts +40 -0
- package/dist/adapters/base.js +11 -0
- package/dist/adapters/fastmcp.d.ts +18 -0
- package/dist/adapters/fastmcp.js +53 -0
- package/dist/adapters/lowlevel.d.ts +41 -0
- package/dist/adapters/lowlevel.js +193 -0
- package/dist/adapters/mcpOfficial.d.ts +20 -0
- package/dist/adapters/mcpOfficial.js +40 -0
- package/dist/capture.d.ts +70 -0
- package/dist/capture.js +266 -0
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +41 -0
- package/dist/dimensions.d.ts +17 -0
- package/dist/dimensions.js +62 -0
- package/dist/export.d.ts +40 -0
- package/dist/export.js +68 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +15 -0
- package/dist/instrument.d.ts +23 -0
- package/dist/instrument.js +183 -0
- package/dist/lifecycle.d.ts +64 -0
- package/dist/lifecycle.js +131 -0
- package/dist/propagation.d.ts +13 -0
- package/dist/propagation.js +33 -0
- package/dist/redaction.d.ts +80 -0
- package/dist/redaction.js +304 -0
- package/package.json +66 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point. Detect framework -> wire pipeline -> wrap server.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent. Fail-open everywhere: nothing here may raise into the customer's
|
|
5
|
+
* request path. The only exception thrown is at install time for an
|
|
6
|
+
* unsupported framework — a developer error, surfaced loudly before any
|
|
7
|
+
* traffic.
|
|
8
|
+
*/
|
|
9
|
+
import type { SessionContext } from './dimensions.js';
|
|
10
|
+
import type { InstrumentHandle } from './lifecycle.js';
|
|
11
|
+
import { RedactionConfig } from './redaction.js';
|
|
12
|
+
export declare const SDK_VERSION = "0.0.1";
|
|
13
|
+
export declare const AGENT_RUNTIME = "vesta-sdk-js";
|
|
14
|
+
export interface InstrumentOptions {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
sessionContext?: SessionContext;
|
|
18
|
+
args?: RedactionConfig;
|
|
19
|
+
responses?: RedactionConfig;
|
|
20
|
+
transport?: string;
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function instrument(server: unknown, options: InstrumentOptions): InstrumentHandle;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point. Detect framework -> wire pipeline -> wrap server.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent. Fail-open everywhere: nothing here may raise into the customer's
|
|
5
|
+
* request path. The only exception thrown is at install time for an
|
|
6
|
+
* unsupported framework — a developer error, surfaced loudly before any
|
|
7
|
+
* traffic.
|
|
8
|
+
*/
|
|
9
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
10
|
+
import { resourceAttributes, spanAttributes } from './capture.js';
|
|
11
|
+
import { sdkLogger, setVerbose } from './diagnostics.js';
|
|
12
|
+
import { DimensionResolver } from './dimensions.js';
|
|
13
|
+
import { CircuitBreaker, buildTracerProvider } from './export.js';
|
|
14
|
+
import { ShutdownManager, makeHandle, NOOP_HANDLE, flushDeadlineMs, resolveTransport, } from './lifecycle.js';
|
|
15
|
+
import { extractUpstream } from './propagation.js';
|
|
16
|
+
import { RedactionConfig, redactPayload } from './redaction.js';
|
|
17
|
+
import { FastMcpAdapter } from './adapters/fastmcp.js';
|
|
18
|
+
import { McpAdapter } from './adapters/mcpOfficial.js';
|
|
19
|
+
export const SDK_VERSION = '0.0.1';
|
|
20
|
+
// The runtime identity stamped on every span (resource service.name ->
|
|
21
|
+
// agent_runtime in the warehouse). Distinct from the Python SDK's "vesta-sdk"
|
|
22
|
+
// so a row's producing SDK is queryable directly on the agent_runtime column,
|
|
23
|
+
// following the existing `vesta-<runtime>` convention (cf. "vesta-shopper").
|
|
24
|
+
// Wire format, payload encoding, and source_kind="sdk" stay identical to the
|
|
25
|
+
// Python SDK — only the runtime label differs.
|
|
26
|
+
export const AGENT_RUNTIME = 'vesta-sdk-js';
|
|
27
|
+
const log = sdkLogger('instrument');
|
|
28
|
+
const ADAPTERS = [new McpAdapter(), new FastMcpAdapter()];
|
|
29
|
+
const DEFAULT_ENDPOINT = 'https://ingest.vesta-analytics.ai/v1/traces';
|
|
30
|
+
const DEFAULT_ARGS = new RedactionConfig({ default: 'redact' });
|
|
31
|
+
const DEFAULT_RESPONSES = new RedactionConfig({ default: 'redact' });
|
|
32
|
+
// Methods whose payload is non-PII handshake/metadata by construction —
|
|
33
|
+
// redaction is bypassed so the captured payload survives intact for analyses
|
|
34
|
+
// that need clientInfo/serverInfo/protocolVersion/capabilities.
|
|
35
|
+
const REDACTION_BYPASS_METHODS = new Set(['initialize']);
|
|
36
|
+
const INSTRUMENTED = Symbol.for('vesta.instrumented');
|
|
37
|
+
function emitSpan(tracer, spanName, attrs, opts) {
|
|
38
|
+
let span = null;
|
|
39
|
+
try {
|
|
40
|
+
span =
|
|
41
|
+
opts.startTime !== undefined
|
|
42
|
+
? tracer.startSpan(spanName, { startTime: opts.startTime })
|
|
43
|
+
: tracer.startSpan(spanName);
|
|
44
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
45
|
+
span.setAttribute(k, v);
|
|
46
|
+
}
|
|
47
|
+
if (opts.isError) {
|
|
48
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (span !== null) {
|
|
53
|
+
if (opts.startTime !== undefined && opts.durationMs !== undefined) {
|
|
54
|
+
span.end(opts.startTime + opts.durationMs);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
span.end();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function instrument(server, options) {
|
|
63
|
+
const target = server;
|
|
64
|
+
if (target && target[INSTRUMENTED]) {
|
|
65
|
+
log.warn('server already instrumented by vesta; ignoring second call');
|
|
66
|
+
return NOOP_HANDLE;
|
|
67
|
+
}
|
|
68
|
+
if (options.verbose) {
|
|
69
|
+
setVerbose(true);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
setVerbose(false);
|
|
73
|
+
}
|
|
74
|
+
const adapter = ADAPTERS.find((a) => a.owns(server));
|
|
75
|
+
if (adapter === undefined) {
|
|
76
|
+
const ctor = server?.constructor?.name ?? typeof server;
|
|
77
|
+
throw new Error(`unsupported MCP server: no Vesta adapter owns ${ctor}. ` +
|
|
78
|
+
'Supported: the official @modelcontextprotocol/sdk (low-level Server and ' +
|
|
79
|
+
'high-level McpServer) and the community fastmcp package.');
|
|
80
|
+
}
|
|
81
|
+
const argsCfg = options.args ?? DEFAULT_ARGS;
|
|
82
|
+
const respCfg = options.responses ?? DEFAULT_RESPONSES;
|
|
83
|
+
const dims = new DimensionResolver(options.sessionContext);
|
|
84
|
+
const breaker = new CircuitBreaker();
|
|
85
|
+
let tracer;
|
|
86
|
+
let manager;
|
|
87
|
+
try {
|
|
88
|
+
const provider = buildTracerProvider({
|
|
89
|
+
endpoint: options.endpoint ?? DEFAULT_ENDPOINT,
|
|
90
|
+
apiKey: options.apiKey,
|
|
91
|
+
resourceAttrs: resourceAttributes({
|
|
92
|
+
agentRuntime: AGENT_RUNTIME,
|
|
93
|
+
agentRuntimeVersion: SDK_VERSION,
|
|
94
|
+
transport: resolveTransport(options.transport),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
tracer = provider.getTracer('vesta-sdk');
|
|
98
|
+
manager = new ShutdownManager(provider, { deadlineMs: flushDeadlineMs() });
|
|
99
|
+
manager.install();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
log.warn('vesta exporter setup failed; instrumentation disabled');
|
|
103
|
+
return NOOP_HANDLE;
|
|
104
|
+
}
|
|
105
|
+
const onInvocation = (inv) => {
|
|
106
|
+
try {
|
|
107
|
+
if (breaker.isOpen())
|
|
108
|
+
return;
|
|
109
|
+
const up = extractUpstream(inv.params);
|
|
110
|
+
// Pass inv.sessionId directly — a falsy/null id means no stable
|
|
111
|
+
// per-session identity, so dimensions resolve fresh rather than share a
|
|
112
|
+
// cache entry across unrelated sessions (cross-user leak fix).
|
|
113
|
+
const sessionDims = dims.forSession(inv.sessionId, { request: inv.request });
|
|
114
|
+
const userId = sessionDims['user_id'] ?? null;
|
|
115
|
+
delete sessionDims['user_id'];
|
|
116
|
+
let scrubbedArgs;
|
|
117
|
+
let scrubbedResult;
|
|
118
|
+
let totalCount = 0;
|
|
119
|
+
let mergedPaths = [];
|
|
120
|
+
if (REDACTION_BYPASS_METHODS.has(inv.method)) {
|
|
121
|
+
scrubbedArgs = inv.arguments;
|
|
122
|
+
scrubbedResult = inv.result;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const a = redactPayload(inv.arguments, { toolName: inv.toolName, method: inv.method, config: argsCfg });
|
|
126
|
+
const r = redactPayload(inv.result, { toolName: inv.toolName, method: inv.method, config: respCfg });
|
|
127
|
+
scrubbedArgs = a.scrubbed;
|
|
128
|
+
scrubbedResult = r.scrubbed;
|
|
129
|
+
totalCount = a.count + r.count;
|
|
130
|
+
mergedPaths = [...a.paths.map((p) => `args:${p}`), ...r.paths.map((p) => `result:${p}`)];
|
|
131
|
+
}
|
|
132
|
+
const attrs = spanAttributes({
|
|
133
|
+
surfaceId: surfaceId(inv),
|
|
134
|
+
toolName: inv.toolName,
|
|
135
|
+
toolArguments: scrubbedArgs,
|
|
136
|
+
toolResult: scrubbedResult,
|
|
137
|
+
sessionId: inv.sessionId,
|
|
138
|
+
sdkVersion: SDK_VERSION,
|
|
139
|
+
dimensions: sessionDims,
|
|
140
|
+
userId,
|
|
141
|
+
linkedTraceId: up.traceId,
|
|
142
|
+
linkedSpanId: up.spanId,
|
|
143
|
+
method: inv.method,
|
|
144
|
+
extraAttributes: inv.extraAttributes ?? null,
|
|
145
|
+
redactionCount: totalCount,
|
|
146
|
+
redactionPaths: mergedPaths,
|
|
147
|
+
});
|
|
148
|
+
const spanName = inv.toolName ? `${inv.method} ${inv.toolName}` : inv.method;
|
|
149
|
+
emitSpan(tracer, spanName, attrs, {
|
|
150
|
+
startTime: inv.startTime,
|
|
151
|
+
durationMs: inv.durationMs,
|
|
152
|
+
isError: inv.isError,
|
|
153
|
+
});
|
|
154
|
+
breaker.recordSuccess();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
breaker.recordFailure();
|
|
158
|
+
log.debug('vesta onInvocation swallowed an exception', err);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
adapter.wrap(server, onInvocation);
|
|
162
|
+
if (target) {
|
|
163
|
+
try {
|
|
164
|
+
Object.defineProperty(target, INSTRUMENTED, {
|
|
165
|
+
value: true,
|
|
166
|
+
enumerable: false,
|
|
167
|
+
configurable: true,
|
|
168
|
+
writable: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
log.debug('vesta could not set instrumentation marker; idempotency protection unavailable');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
log.debug(`vesta instrumented ${typeof server} via ${adapter.name}`);
|
|
176
|
+
return makeHandle(manager);
|
|
177
|
+
}
|
|
178
|
+
function surfaceId(_inv) {
|
|
179
|
+
// surface_id is issued manually in v0 and resolved via api-key->surface
|
|
180
|
+
// mapping at ingest. The SDK stamps a placeholder the ingest static map
|
|
181
|
+
// authorises.
|
|
182
|
+
return 'surf_dev';
|
|
183
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-shutdown lifecycle for the SDK exporter.
|
|
3
|
+
*
|
|
4
|
+
* Node has no atexit and BasicTracerProvider registers no exit flush, so for a
|
|
5
|
+
* stdio MCP subprocess the entire delivery path is a bounded flush on shutdown.
|
|
6
|
+
* This module owns the provider instrument() builds, hooks the shutdown
|
|
7
|
+
* triggers, and bounds exit (the OTLP exporter's own timeout does not bound
|
|
8
|
+
* wall-clock). Fail-open throughout: nothing here throws into the host or
|
|
9
|
+
* blocks exit past the deadline.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_DEADLINE_MS = 2000;
|
|
12
|
+
export interface ShutdownProvider {
|
|
13
|
+
forceFlush(): Promise<void>;
|
|
14
|
+
shutdown(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface ProcessLike {
|
|
17
|
+
on(event: string, listener: (...a: unknown[]) => void): unknown;
|
|
18
|
+
listenerCount(event: string): number;
|
|
19
|
+
exit(code?: number): void;
|
|
20
|
+
stdin: {
|
|
21
|
+
once(event: string, listener: (...a: unknown[]) => void): unknown;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare class ShutdownManager {
|
|
25
|
+
private readonly provider;
|
|
26
|
+
private readonly deadlineMs;
|
|
27
|
+
private readonly proc;
|
|
28
|
+
private shutdownStarted;
|
|
29
|
+
private installed;
|
|
30
|
+
constructor(provider: ShutdownProvider, opts?: {
|
|
31
|
+
deadlineMs?: number;
|
|
32
|
+
proc?: ProcessLike;
|
|
33
|
+
});
|
|
34
|
+
/** Deadline-bounded forceFlush. No-op (true) once shutdown started. Never throws. */
|
|
35
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
36
|
+
/** Bounded flush then provider shutdown, run once. Never throws. */
|
|
37
|
+
shutdown(): Promise<void>;
|
|
38
|
+
/** Register shutdown hooks. Idempotent; fail-open per hook. */
|
|
39
|
+
install(): void;
|
|
40
|
+
private tryReg;
|
|
41
|
+
/** stdin closed (EOF) — the common stdio session end. Always flush + exit. */
|
|
42
|
+
handleEof(): Promise<void>;
|
|
43
|
+
/** SIGTERM/SIGINT — flush, then exit only if we are the sole listener (a
|
|
44
|
+
* stdio process with nothing to defer to). Otherwise leave termination to
|
|
45
|
+
* the other handler (e.g. an HTTP framework's graceful drain). */
|
|
46
|
+
handleSignal(sig: string): Promise<void>;
|
|
47
|
+
/** Run fn raced against a timer. Resolves true if fn settled within the
|
|
48
|
+
* deadline without rejecting, false if it rejected or the deadline won. */
|
|
49
|
+
private bounded;
|
|
50
|
+
}
|
|
51
|
+
export interface InstrumentHandle {
|
|
52
|
+
/** Bounded force-flush. True if completed within the deadline. Never throws. */
|
|
53
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
54
|
+
/** Bounded flush then shutdown. Idempotent. Never throws. */
|
|
55
|
+
shutdown(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function makeHandle(mgr: ShutdownManager): InstrumentHandle;
|
|
58
|
+
/** Returned by instrument() when instrumentation is disabled (already-instrumented
|
|
59
|
+
* or exporter-setup failure), so the return type stays non-nullable. */
|
|
60
|
+
export declare const NOOP_HANDLE: InstrumentHandle;
|
|
61
|
+
export declare function flushDeadlineMs(): number;
|
|
62
|
+
/** Best-effort transport label for vesta.transport. Label only; a wrong value
|
|
63
|
+
* is harmless. Heuristic returns 'stdio' or 'unknown', never 'http'. */
|
|
64
|
+
export declare function resolveTransport(explicit?: string): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-shutdown lifecycle for the SDK exporter.
|
|
3
|
+
*
|
|
4
|
+
* Node has no atexit and BasicTracerProvider registers no exit flush, so for a
|
|
5
|
+
* stdio MCP subprocess the entire delivery path is a bounded flush on shutdown.
|
|
6
|
+
* This module owns the provider instrument() builds, hooks the shutdown
|
|
7
|
+
* triggers, and bounds exit (the OTLP exporter's own timeout does not bound
|
|
8
|
+
* wall-clock). Fail-open throughout: nothing here throws into the host or
|
|
9
|
+
* blocks exit past the deadline.
|
|
10
|
+
*/
|
|
11
|
+
import { sdkLogger } from './diagnostics.js';
|
|
12
|
+
const log = sdkLogger('lifecycle');
|
|
13
|
+
export const DEFAULT_DEADLINE_MS = 2000;
|
|
14
|
+
export class ShutdownManager {
|
|
15
|
+
provider;
|
|
16
|
+
deadlineMs;
|
|
17
|
+
proc;
|
|
18
|
+
shutdownStarted = false;
|
|
19
|
+
installed = false;
|
|
20
|
+
constructor(provider, opts = {}) {
|
|
21
|
+
this.provider = provider;
|
|
22
|
+
this.deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
23
|
+
this.proc = opts.proc ?? process;
|
|
24
|
+
}
|
|
25
|
+
/** Deadline-bounded forceFlush. No-op (true) once shutdown started. Never throws. */
|
|
26
|
+
async flush(timeoutMs) {
|
|
27
|
+
if (this.shutdownStarted)
|
|
28
|
+
return true;
|
|
29
|
+
return this.bounded(() => this.provider.forceFlush(), timeoutMs ?? this.deadlineMs);
|
|
30
|
+
}
|
|
31
|
+
/** Bounded flush then provider shutdown, run once. Never throws. */
|
|
32
|
+
async shutdown() {
|
|
33
|
+
if (this.shutdownStarted)
|
|
34
|
+
return;
|
|
35
|
+
this.shutdownStarted = true;
|
|
36
|
+
await this.bounded(() => this.provider.forceFlush(), this.deadlineMs);
|
|
37
|
+
await this.bounded(() => this.provider.shutdown(), this.deadlineMs);
|
|
38
|
+
}
|
|
39
|
+
/** Register shutdown hooks. Idempotent; fail-open per hook. */
|
|
40
|
+
install() {
|
|
41
|
+
if (this.installed)
|
|
42
|
+
return;
|
|
43
|
+
this.installed = true;
|
|
44
|
+
this.tryReg(() => this.proc.stdin.once('end', () => void this.handleEof()));
|
|
45
|
+
this.tryReg(() => this.proc.on('SIGTERM', () => void this.handleSignal('SIGTERM')));
|
|
46
|
+
this.tryReg(() => this.proc.on('SIGINT', () => void this.handleSignal('SIGINT')));
|
|
47
|
+
}
|
|
48
|
+
tryReg(fn) {
|
|
49
|
+
try {
|
|
50
|
+
fn();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
log.debug('vesta: shutdown hook registration failed', err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** stdin closed (EOF) — the common stdio session end. Always flush + exit. */
|
|
57
|
+
async handleEof() {
|
|
58
|
+
await this.shutdown();
|
|
59
|
+
this.proc.exit(0);
|
|
60
|
+
}
|
|
61
|
+
/** SIGTERM/SIGINT — flush, then exit only if we are the sole listener (a
|
|
62
|
+
* stdio process with nothing to defer to). Otherwise leave termination to
|
|
63
|
+
* the other handler (e.g. an HTTP framework's graceful drain). */
|
|
64
|
+
async handleSignal(sig) {
|
|
65
|
+
// Snapshot co-listener state at signal time, before the await: another
|
|
66
|
+
// handler must not be able to deregister and flip the sole-listener test.
|
|
67
|
+
const sole = this.proc.listenerCount(sig) === 1;
|
|
68
|
+
await this.shutdown();
|
|
69
|
+
if (sole)
|
|
70
|
+
this.proc.exit(0);
|
|
71
|
+
}
|
|
72
|
+
/** Run fn raced against a timer. Resolves true if fn settled within the
|
|
73
|
+
* deadline without rejecting, false if it rejected or the deadline won. */
|
|
74
|
+
async bounded(fn, deadlineMs) {
|
|
75
|
+
let timer;
|
|
76
|
+
const timeout = new Promise((resolve) => {
|
|
77
|
+
timer = setTimeout(() => resolve(false), deadlineMs);
|
|
78
|
+
});
|
|
79
|
+
const run = fn().then(() => true, (err) => {
|
|
80
|
+
log.debug('vesta bounded shutdown op rejected', err);
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
83
|
+
const ok = await Promise.race([run, timeout]);
|
|
84
|
+
if (timer !== undefined)
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
// When the deadline wins, `run` (and its in-flight OTLP socket) is left
|
|
87
|
+
// unsettled by design — the caller exits the process immediately after,
|
|
88
|
+
// which reaps it. Do not "fix" this by awaiting `run`; that reintroduces
|
|
89
|
+
// the unbounded hang this race exists to prevent.
|
|
90
|
+
return ok;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function makeHandle(mgr) {
|
|
94
|
+
return {
|
|
95
|
+
flush: (timeoutMs) => mgr.flush(timeoutMs),
|
|
96
|
+
shutdown: () => mgr.shutdown(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Returned by instrument() when instrumentation is disabled (already-instrumented
|
|
100
|
+
* or exporter-setup failure), so the return type stays non-nullable. */
|
|
101
|
+
export const NOOP_HANDLE = {
|
|
102
|
+
flush: () => Promise.resolve(true),
|
|
103
|
+
shutdown: () => Promise.resolve(),
|
|
104
|
+
};
|
|
105
|
+
export function flushDeadlineMs() {
|
|
106
|
+
const raw = process.env.VESTA_FLUSH_DEADLINE_MS;
|
|
107
|
+
if (raw === undefined || raw === '')
|
|
108
|
+
return DEFAULT_DEADLINE_MS;
|
|
109
|
+
const value = Number.parseInt(raw, 10);
|
|
110
|
+
if (Number.isFinite(value) && value > 0)
|
|
111
|
+
return value;
|
|
112
|
+
log.debug(`invalid VESTA_FLUSH_DEADLINE_MS=${raw}; using default`);
|
|
113
|
+
return DEFAULT_DEADLINE_MS;
|
|
114
|
+
}
|
|
115
|
+
/** Best-effort transport label for vesta.transport. Label only; a wrong value
|
|
116
|
+
* is harmless. Heuristic returns 'stdio' or 'unknown', never 'http'. */
|
|
117
|
+
export function resolveTransport(explicit) {
|
|
118
|
+
if (explicit)
|
|
119
|
+
return explicit;
|
|
120
|
+
const env = process.env.VESTA_TRANSPORT;
|
|
121
|
+
if (env)
|
|
122
|
+
return env;
|
|
123
|
+
try {
|
|
124
|
+
if (!process.stdin.isTTY)
|
|
125
|
+
return 'stdio';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* fall through to unknown */
|
|
129
|
+
}
|
|
130
|
+
return 'unknown';
|
|
131
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP propagates trace context inside `params._meta` of the JSON-RPC body, not
|
|
3
|
+
* in HTTP headers. The format is in-flight in the MCP spec — parse
|
|
4
|
+
* permissively, never throw, fall back to Server-only on anything unexpected.
|
|
5
|
+
*/
|
|
6
|
+
export interface Upstream {
|
|
7
|
+
linked: boolean;
|
|
8
|
+
traceId: string | null;
|
|
9
|
+
spanId: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Read the W3C `traceparent` from `params._meta`. Permissive; fail to
|
|
12
|
+
* Server-only. `params` is the raw JSON-RPC params object. */
|
|
13
|
+
export declare function extractUpstream(params: unknown): Upstream;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP propagates trace context inside `params._meta` of the JSON-RPC body, not
|
|
3
|
+
* in HTTP headers. The format is in-flight in the MCP spec — parse
|
|
4
|
+
* permissively, never throw, fall back to Server-only on anything unexpected.
|
|
5
|
+
*/
|
|
6
|
+
const SERVER_ONLY = { linked: false, traceId: null, spanId: null };
|
|
7
|
+
function isHex(s) {
|
|
8
|
+
return /^[0-9a-fA-F]+$/.test(s);
|
|
9
|
+
}
|
|
10
|
+
/** Read the W3C `traceparent` from `params._meta`. Permissive; fail to
|
|
11
|
+
* Server-only. `params` is the raw JSON-RPC params object. */
|
|
12
|
+
export function extractUpstream(params) {
|
|
13
|
+
try {
|
|
14
|
+
if (typeof params !== 'object' || params === null)
|
|
15
|
+
return SERVER_ONLY;
|
|
16
|
+
const meta = params['_meta'];
|
|
17
|
+
if (typeof meta !== 'object' || meta === null)
|
|
18
|
+
return SERVER_ONLY;
|
|
19
|
+
const tp = meta['traceparent'];
|
|
20
|
+
if (typeof tp !== 'string')
|
|
21
|
+
return SERVER_ONLY;
|
|
22
|
+
const parts = tp.split('-');
|
|
23
|
+
if (parts.length !== 4 || parts[1].length !== 32 || parts[2].length !== 16) {
|
|
24
|
+
return SERVER_ONLY;
|
|
25
|
+
}
|
|
26
|
+
if (!isHex(parts[1]) || !isHex(parts[2]))
|
|
27
|
+
return SERVER_ONLY;
|
|
28
|
+
return { linked: true, traceId: parts[1], spanId: parts[2] };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return SERVER_ONLY;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction engine. Pure — no framework or OTel imports.
|
|
3
|
+
*
|
|
4
|
+
* Precedence: field rules > subject rules (tool/prompt/resource) > method
|
|
5
|
+
* rules > default + denylist. Within field rules: exact path beats glob;
|
|
6
|
+
* among globs, the longer pattern wins. The shipped denylist behaves as
|
|
7
|
+
* implicit lowest-precedence field redacts, only relevant when
|
|
8
|
+
* default === "capture". This is a 1:1 port of the Python SDK's `redaction.py`.
|
|
9
|
+
*/
|
|
10
|
+
export type ActionName = 'capture' | 'redact' | 'hash' | 'truncate';
|
|
11
|
+
export declare const REDACTED = "<REDACTED>";
|
|
12
|
+
export declare const DEFAULT_DENYLIST: ReadonlySet<string>;
|
|
13
|
+
interface Action {
|
|
14
|
+
name: ActionName;
|
|
15
|
+
n?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Render a scalar the way Python's `repr()` does, so the `hash` action
|
|
19
|
+
* produces the same digest across the Python and TypeScript SDKs for the
|
|
20
|
+
* common scalar cases (string, integer, float-with-fraction, bool, null).
|
|
21
|
+
* Exotic types are a documented divergence — `_leaves` only ever hands us
|
|
22
|
+
* scalars, so this covers redaction in practice.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pyRepr(value: unknown): string;
|
|
25
|
+
type RuleLevel = 'field' | 'tool' | 'prompt' | 'resource' | 'method';
|
|
26
|
+
export type Predicate = (path: string, value: unknown) => string;
|
|
27
|
+
export interface Rule {
|
|
28
|
+
level: RuleLevel;
|
|
29
|
+
pattern: string;
|
|
30
|
+
action: Action | null;
|
|
31
|
+
predicate: Predicate | null;
|
|
32
|
+
rx: RegExp;
|
|
33
|
+
/** exact (no glob char) beats glob; then longer pattern wins. */
|
|
34
|
+
specificity: [number, number];
|
|
35
|
+
}
|
|
36
|
+
declare class Builder {
|
|
37
|
+
private readonly level;
|
|
38
|
+
private readonly pattern;
|
|
39
|
+
constructor(level: RuleLevel, pattern: string);
|
|
40
|
+
private mk;
|
|
41
|
+
capture(): Rule;
|
|
42
|
+
redact(): Rule;
|
|
43
|
+
hash(): Rule;
|
|
44
|
+
truncate(n: number): Rule;
|
|
45
|
+
check(predicate: Predicate): Rule;
|
|
46
|
+
}
|
|
47
|
+
export declare function Field(pattern: string): Builder;
|
|
48
|
+
/** Match by tool name. Implicitly scoped to method=tools/call. */
|
|
49
|
+
export declare function Tool(pattern: string): Builder;
|
|
50
|
+
/** Match by prompt name. Scoped to method=prompts/get. */
|
|
51
|
+
export declare function Prompt(pattern: string): Builder;
|
|
52
|
+
/** Match by resource URI glob. Scoped to method=resources/read. */
|
|
53
|
+
export declare function Resource(pattern: string): Builder;
|
|
54
|
+
/** Match by MCP method name (`tools/list`, `prompts/*`, …). */
|
|
55
|
+
export declare function Method(pattern: string): Builder;
|
|
56
|
+
export interface RedactionConfigInit {
|
|
57
|
+
default?: string;
|
|
58
|
+
rules?: Rule[];
|
|
59
|
+
}
|
|
60
|
+
export declare class RedactionConfig {
|
|
61
|
+
readonly default: string;
|
|
62
|
+
readonly rules: Rule[];
|
|
63
|
+
readonly defaultAction: Action;
|
|
64
|
+
readonly fieldRules: Rule[];
|
|
65
|
+
readonly subjectRules: Record<string, Rule[]>;
|
|
66
|
+
readonly methodRules: Rule[];
|
|
67
|
+
constructor(init?: RedactionConfigInit);
|
|
68
|
+
}
|
|
69
|
+
export interface RedactResult {
|
|
70
|
+
scrubbed: unknown;
|
|
71
|
+
count: number;
|
|
72
|
+
paths: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface RedactOptions {
|
|
75
|
+
toolName: string;
|
|
76
|
+
config: RedactionConfig;
|
|
77
|
+
method?: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function redactPayload(payload: unknown, opts: RedactOptions): RedactResult;
|
|
80
|
+
export {};
|