@revealui/ai 0.2.9 → 0.4.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/README.md +200 -26
- package/dist/a2a/handler.d.ts +3 -1
- package/dist/a2a/handler.d.ts.map +1 -1
- package/dist/a2a/handler.js +24 -7
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +5 -1
- package/dist/client/hooks/useAgentStream.d.ts +84 -1
- package/dist/client/hooks/useAgentStream.d.ts.map +1 -1
- package/dist/client/hooks/useAgentStream.js +106 -10
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -15
- package/dist/ingestion/admin-indexer.js +1 -1
- package/dist/ingestion/pipeline.d.ts +1 -1
- package/dist/ingestion/pipeline.d.ts.map +1 -1
- package/dist/llm/client.d.ts +33 -10
- package/dist/llm/client.d.ts.map +1 -1
- package/dist/llm/client.js +70 -57
- package/dist/llm/key-validator.d.ts.map +1 -1
- package/dist/llm/key-validator.js +0 -27
- package/dist/llm/providers/inference-snaps.d.ts +7 -5
- package/dist/llm/providers/inference-snaps.d.ts.map +1 -1
- package/dist/llm/providers/inference-snaps.js +7 -5
- package/dist/llm/providers/openai-compat.d.ts +1 -1
- package/dist/llm/providers/openai-compat.js +1 -1
- package/dist/llm/response-cache.d.ts +1 -2
- package/dist/llm/response-cache.d.ts.map +1 -1
- package/dist/llm/response-cache.js +1 -2
- package/dist/llm/server.d.ts +1 -1
- package/dist/llm/server.d.ts.map +1 -1
- package/dist/llm/server.js +3 -1
- package/dist/memory/crdt/lww-register.js +1 -1
- package/dist/memory/preferences/user-preferences-manager.d.ts.map +1 -1
- package/dist/memory/preferences/user-preferences-manager.js +1 -1
- package/dist/memory/services/index.d.ts +2 -0
- package/dist/memory/services/index.d.ts.map +1 -1
- package/dist/memory/services/index.js +1 -0
- package/dist/memory/services/reconciliation-service.d.ts +64 -0
- package/dist/memory/services/reconciliation-service.d.ts.map +1 -0
- package/dist/memory/services/reconciliation-service.js +127 -0
- package/dist/memory/stores/working-memory.d.ts.map +1 -1
- package/dist/memory/stores/working-memory.js +1 -1
- package/dist/memory/utils/index.d.ts +1 -2
- package/dist/memory/utils/index.d.ts.map +1 -1
- package/dist/memory/utils/index.js +1 -2
- package/dist/memory/utils/sql-helpers.d.ts +21 -66
- package/dist/memory/utils/sql-helpers.d.ts.map +1 -1
- package/dist/memory/utils/sql-helpers.js +34 -169
- package/dist/memory/vector/vector-memory-service.d.ts.map +1 -1
- package/dist/memory/vector/vector-memory-service.js +6 -1
- package/dist/orchestration/runtime.d.ts +30 -1
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +20 -3
- package/dist/orchestration/streaming-runtime.d.ts +48 -1
- package/dist/orchestration/streaming-runtime.d.ts.map +1 -1
- package/dist/orchestration/ticket-agent.d.ts +10 -1
- package/dist/orchestration/ticket-agent.d.ts.map +1 -1
- package/dist/orchestration/ticket-agent.js +11 -2
- package/dist/skills/catalog/vercel-catalog.d.ts.map +1 -1
- package/dist/skills/catalog/vercel-catalog.js +13 -7
- package/dist/tools/mcp-adapter.d.ts +192 -1
- package/dist/tools/mcp-adapter.d.ts.map +1 -1
- package/dist/tools/mcp-adapter.js +434 -1
- package/dist/tools/mcp-elicitation.d.ts +128 -0
- package/dist/tools/mcp-elicitation.d.ts.map +1 -0
- package/dist/tools/mcp-elicitation.js +121 -0
- package/dist/tools/mcp-events.d.ts +270 -0
- package/dist/tools/mcp-events.d.ts.map +1 -0
- package/dist/tools/mcp-events.js +165 -0
- package/dist/tools/mcp-sampling.d.ts +155 -0
- package/dist/tools/mcp-sampling.d.ts.map +1 -0
- package/dist/tools/mcp-sampling.js +171 -0
- package/dist/tools/ticket-tools.d.ts +35 -2
- package/dist/tools/ticket-tools.d.ts.map +1 -1
- package/dist/tools/ticket-tools.js +32 -2
- package/package.json +38 -12
- package/dist/llm/providers/vultr.d.ts +0 -18
- package/dist/llm/providers/vultr.d.ts.map +0 -1
- package/dist/llm/providers/vultr.js +0 -169
- package/dist/memory/utils/deep-clone.d.ts +0 -10
- package/dist/memory/utils/deep-clone.d.ts.map +0 -1
- package/dist/memory/utils/deep-clone.js +0 -9
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Elicitation — route server `elicitation/create` requests through
|
|
3
|
+
* a consumer-provided UI callback (Stage 5.3 of the MCP v1 plan).
|
|
4
|
+
*
|
|
5
|
+
* The MCP spec defines `elicitation/create` as a server-to-client
|
|
6
|
+
* request: a server asks the client to collect structured input from
|
|
7
|
+
* the user mid-flow (form inputs, confirmations, auth prompts). The
|
|
8
|
+
* agent runtime is NOT the right place to decide what UI to show —
|
|
9
|
+
* each consumer (admin inspector, agent execution panel, CLI tool,
|
|
10
|
+
* Slack bot, …) renders in its own idiom. This module exposes
|
|
11
|
+
* `createElicitationHandler({ onElicit })` — a thin factory that
|
|
12
|
+
* wraps the consumer's async UI callback into the structural handler
|
|
13
|
+
* shape that `McpClient` from `@revealui/mcp/client` expects.
|
|
14
|
+
*
|
|
15
|
+
* Safety defaults
|
|
16
|
+
* ---------------
|
|
17
|
+
* - Servers that request **URL mode** (out-of-band consent via a
|
|
18
|
+
* separate browser tab) are auto-declined unless `allowUrlMode: true`
|
|
19
|
+
* is passed. URL mode is a phishing vector when the user can't
|
|
20
|
+
* easily verify what domain they're on — admin inspector (Stage 3.4)
|
|
21
|
+
* takes the same posture; agent runtime matches for consistency.
|
|
22
|
+
* - Optional `timeoutMs` auto-declines after the deadline. Useful in
|
|
23
|
+
* headless automation where no human is at the keyboard.
|
|
24
|
+
* - Thrown errors inside `onElicit` are converted to `{ action:
|
|
25
|
+
* 'cancel' }` rather than propagating — servers should see a clean
|
|
26
|
+
* decline, not a protocol error the UI crashed on.
|
|
27
|
+
*
|
|
28
|
+
* As with the rest of the MCP-adapter surface, this module uses
|
|
29
|
+
* structural typing to stay decoupled from `@revealui/mcp` — the real
|
|
30
|
+
* `ElicitationHandler` from `@revealui/mcp/client` structurally
|
|
31
|
+
* satisfies the `McpElicitationHandler` shape exported here.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { McpClient } from '@revealui/mcp/client';
|
|
36
|
+
* import { createElicitationHandler } from '@revealui/ai';
|
|
37
|
+
*
|
|
38
|
+
* const client = new McpClient({
|
|
39
|
+
* clientInfo: { name: 'my-agent', version: '1.0.0' },
|
|
40
|
+
* transport: { kind: 'streamable-http', url: '…' },
|
|
41
|
+
* elicitationHandler: createElicitationHandler({
|
|
42
|
+
* onElicit: async ({ message, requestedSchema }) => {
|
|
43
|
+
* const form = await showFormDialog({ title: message, schema: requestedSchema });
|
|
44
|
+
* if (!form) return { action: 'cancel' };
|
|
45
|
+
* return { action: 'accept', content: form.values };
|
|
46
|
+
* },
|
|
47
|
+
* timeoutMs: 60_000,
|
|
48
|
+
* }),
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
import { logger } from '@revealui/core/observability/logger';
|
|
53
|
+
import { emitMcpEvent } from './mcp-events.js';
|
|
54
|
+
export function createElicitationHandler(options) {
|
|
55
|
+
const { onElicit, timeoutMs, allowUrlMode, onElicitationRequest, onEvent, namespace } = options;
|
|
56
|
+
return async (params) => {
|
|
57
|
+
const fieldCount = countSchemaFields(params.requestedSchema);
|
|
58
|
+
const started = Date.now();
|
|
59
|
+
if (params.mode === 'url' && !allowUrlMode) {
|
|
60
|
+
emitMcpEvent(onEvent, {
|
|
61
|
+
kind: 'mcp.elicitation.create',
|
|
62
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
63
|
+
action: 'decline',
|
|
64
|
+
fieldCount,
|
|
65
|
+
...(params.mode !== undefined ? { mode: params.mode } : {}),
|
|
66
|
+
duration_ms: Date.now() - started,
|
|
67
|
+
success: true,
|
|
68
|
+
});
|
|
69
|
+
return { action: 'decline' };
|
|
70
|
+
}
|
|
71
|
+
onElicitationRequest?.({
|
|
72
|
+
message: params.message,
|
|
73
|
+
fieldCount,
|
|
74
|
+
...(params.mode !== undefined ? { mode: params.mode } : {}),
|
|
75
|
+
});
|
|
76
|
+
let timeoutHandle;
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
const userResponse = onElicit(params).catch((error) => {
|
|
80
|
+
// Errors inside the consumer's UI callback → cancel (not decline —
|
|
81
|
+
// the server should see a clean "didn't happen", not a
|
|
82
|
+
// misrepresented "user said no").
|
|
83
|
+
logger.warn('[createElicitationHandler] onElicit threw; returning cancel', {
|
|
84
|
+
error: error instanceof Error ? error.message : String(error),
|
|
85
|
+
});
|
|
86
|
+
return { action: 'cancel' };
|
|
87
|
+
});
|
|
88
|
+
if (timeoutMs === undefined || timeoutMs <= 0) {
|
|
89
|
+
result = await userResponse;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
93
|
+
timeoutHandle = setTimeout(() => {
|
|
94
|
+
resolve({ action: 'cancel' });
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
});
|
|
97
|
+
result = await Promise.race([userResponse, timeoutPromise]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timeoutHandle !== undefined)
|
|
102
|
+
clearTimeout(timeoutHandle);
|
|
103
|
+
}
|
|
104
|
+
emitMcpEvent(onEvent, {
|
|
105
|
+
kind: 'mcp.elicitation.create',
|
|
106
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
107
|
+
action: result.action,
|
|
108
|
+
fieldCount,
|
|
109
|
+
...(params.mode !== undefined ? { mode: params.mode } : {}),
|
|
110
|
+
duration_ms: Date.now() - started,
|
|
111
|
+
success: true,
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Count the declared fields on a `requestedSchema.properties` record. */
|
|
117
|
+
function countSchemaFields(schema) {
|
|
118
|
+
if (!(schema?.properties && typeof schema.properties === 'object'))
|
|
119
|
+
return 0;
|
|
120
|
+
return Object.keys(schema.properties).length;
|
|
121
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol log channel — structured events per server-to-client call
|
|
3
|
+
* (Stage 6.1 of the MCP v1 plan).
|
|
4
|
+
*
|
|
5
|
+
* Stage 5 shipped the full agent-side MCP protocol surface
|
|
6
|
+
* (tools + resources + prompts + sampling + elicitation + progress +
|
|
7
|
+
* cancellation). Stage 6.1 adds observability: every adapter in
|
|
8
|
+
* `@revealui/ai/tools/mcp-*` grows an `onEvent?: McpEventSink` hook that
|
|
9
|
+
* fires once per protocol call with a structured summary. Consumers wire
|
|
10
|
+
* the sink to whatever aggregator they run — typically
|
|
11
|
+
* `@revealui/core/observability/logger` via `createCoreLoggerSink()`.
|
|
12
|
+
*
|
|
13
|
+
* Instrumentation lives at the agent-adapter layer by design. The
|
|
14
|
+
* hypervisor is a separate lane (Stage 6.2 lands `usage_meters` rows
|
|
15
|
+
* there, where `accountId` is in scope). Agent adapters are intentionally
|
|
16
|
+
* tenant-agnostic, so events carry no `accountId` field — the consumer
|
|
17
|
+
* adds tenant context in their sink wrapper if they need it for routing.
|
|
18
|
+
*
|
|
19
|
+
* PII note
|
|
20
|
+
* --------
|
|
21
|
+
* Errors surfaced by the server can legitimately contain user-identifying
|
|
22
|
+
* strings (e.g. "user 'alice@example.com' not found"). `error` is passed
|
|
23
|
+
* through to the sink verbatim. If your deployment's observability layer
|
|
24
|
+
* is subject to PII constraints, wrap the sink with a redactor
|
|
25
|
+
* (`redactLogContext` from `@revealui/security`) before passing it in.
|
|
26
|
+
*
|
|
27
|
+
* Params / args / content bodies are **not** included in events —
|
|
28
|
+
* observability is summary-grained by default to avoid accidentally
|
|
29
|
+
* logging credentials, prompts, or document contents.
|
|
30
|
+
*/
|
|
31
|
+
/** Common fields every MCP protocol-log event carries. */
|
|
32
|
+
interface McpEventBase {
|
|
33
|
+
/**
|
|
34
|
+
* Wall-clock duration of the call in milliseconds, measured from
|
|
35
|
+
* handler entry to handler exit (success or failure). Includes the
|
|
36
|
+
* full round-trip across the transport plus any zod validation.
|
|
37
|
+
*/
|
|
38
|
+
duration_ms: number;
|
|
39
|
+
/** `true` when the call completed without throwing and without `isError`. */
|
|
40
|
+
success: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Server-reported error text on failure (from `CallToolResult.content`
|
|
43
|
+
* when `isError: true`, or from `error.message` when the call threw).
|
|
44
|
+
* Omitted when `success: true`. May contain PII — see module docs.
|
|
45
|
+
*/
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface McpToolCallEvent extends McpEventBase {
|
|
49
|
+
kind: 'mcp.tool.call';
|
|
50
|
+
/** Server identifier the tool belongs to. */
|
|
51
|
+
namespace: string;
|
|
52
|
+
/** Tool name WITHOUT the `mcp_<namespace>__` adapter prefix. */
|
|
53
|
+
toolName: string;
|
|
54
|
+
}
|
|
55
|
+
export interface McpResourceListEvent extends McpEventBase {
|
|
56
|
+
kind: 'mcp.resource.list';
|
|
57
|
+
namespace: string;
|
|
58
|
+
/** Number of resources returned (omitted on failure). */
|
|
59
|
+
resourceCount?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface McpResourceReadEvent extends McpEventBase {
|
|
62
|
+
kind: 'mcp.resource.read';
|
|
63
|
+
namespace: string;
|
|
64
|
+
/** Resource URI that was read. May be sensitive — see module docs. */
|
|
65
|
+
uri: string;
|
|
66
|
+
}
|
|
67
|
+
export interface McpPromptListEvent extends McpEventBase {
|
|
68
|
+
kind: 'mcp.prompt.list';
|
|
69
|
+
namespace: string;
|
|
70
|
+
/** Number of prompts returned (omitted on failure). */
|
|
71
|
+
promptCount?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface McpPromptGetEvent extends McpEventBase {
|
|
74
|
+
kind: 'mcp.prompt.get';
|
|
75
|
+
namespace: string;
|
|
76
|
+
/** Prompt name that was requested. */
|
|
77
|
+
promptName: string;
|
|
78
|
+
}
|
|
79
|
+
export interface McpSamplingCreateEvent extends McpEventBase {
|
|
80
|
+
kind: 'mcp.sampling.create';
|
|
81
|
+
/**
|
|
82
|
+
* Server identifier when the consumer attached one at handler
|
|
83
|
+
* construction via `createSamplingHandler({ namespace })`. Absent
|
|
84
|
+
* when the handler is wired to a shared `McpClient` that fans out
|
|
85
|
+
* to multiple servers — in that case the consumer's sink wrapper
|
|
86
|
+
* can fill it in from call-site context.
|
|
87
|
+
*/
|
|
88
|
+
namespace?: string;
|
|
89
|
+
/** Model label reported back to the server in `result.model`. */
|
|
90
|
+
model: string;
|
|
91
|
+
messageCount: number;
|
|
92
|
+
maxTokens: number;
|
|
93
|
+
}
|
|
94
|
+
export interface McpElicitationCreateEvent extends McpEventBase {
|
|
95
|
+
kind: 'mcp.elicitation.create';
|
|
96
|
+
/** Server identifier when provided to `createElicitationHandler`. */
|
|
97
|
+
namespace?: string;
|
|
98
|
+
/** User's decision — `'accept' | 'decline' | 'cancel'`. */
|
|
99
|
+
action: 'accept' | 'decline' | 'cancel';
|
|
100
|
+
/** Number of fields in the requested schema. */
|
|
101
|
+
fieldCount: number;
|
|
102
|
+
/** Elicitation mode as declared by the server (`'form'`, `'url'`, …). */
|
|
103
|
+
mode?: string;
|
|
104
|
+
}
|
|
105
|
+
export type McpLogEvent = McpToolCallEvent | McpResourceListEvent | McpResourceReadEvent | McpPromptListEvent | McpPromptGetEvent | McpSamplingCreateEvent | McpElicitationCreateEvent;
|
|
106
|
+
/**
|
|
107
|
+
* Consumer-wired observability sink. Called exactly once per protocol
|
|
108
|
+
* call. Must not throw — the adapter swallows sink-thrown errors
|
|
109
|
+
* internally, but a well-behaved sink returns quickly and cleanly.
|
|
110
|
+
*/
|
|
111
|
+
export type McpEventSink = (event: McpLogEvent) => void;
|
|
112
|
+
export interface CreateCoreLoggerSinkOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Minimum level for successful events. Default `'info'`. Set to
|
|
115
|
+
* `'debug'` to drop successful-call volume from info-level ingestion.
|
|
116
|
+
* Failed calls are always emitted at `'warn'` regardless.
|
|
117
|
+
*/
|
|
118
|
+
successLevel?: 'debug' | 'info';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build an `McpEventSink` that fans events into the central log
|
|
122
|
+
* aggregator via `@revealui/core/observability/logger`. This is the
|
|
123
|
+
* recommended default for in-process consumers (admin app, API, agent
|
|
124
|
+
* orchestration). Out-of-process consumers (CLI, remote inspector)
|
|
125
|
+
* typically construct a custom sink that forwards to their own transport.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* import {
|
|
130
|
+
* createToolsFromMcpClient,
|
|
131
|
+
* createCoreLoggerSink,
|
|
132
|
+
* } from '@revealui/ai';
|
|
133
|
+
*
|
|
134
|
+
* const onEvent = createCoreLoggerSink();
|
|
135
|
+
*
|
|
136
|
+
* const tools = await createToolsFromMcpClient(client, {
|
|
137
|
+
* namespace: 'my-server',
|
|
138
|
+
* onEvent,
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export declare function createCoreLoggerSink(options?: CreateCoreLoggerSinkOptions): McpEventSink;
|
|
143
|
+
/**
|
|
144
|
+
* Invoke a sink, swallowing any thrown error. A misbehaving sink must
|
|
145
|
+
* never break the underlying MCP call — observability is best-effort.
|
|
146
|
+
* Logs the sink failure at warn so the regression is still visible.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export declare function emitMcpEvent(sink: McpEventSink | undefined, event: McpLogEvent): void;
|
|
151
|
+
/**
|
|
152
|
+
* Row shape consumed by the sink's `write` callback. Structurally
|
|
153
|
+
* matches `usage_meters` insert payload in `@revealui/db/schema/accounts`.
|
|
154
|
+
* Kept as a plain interface here to avoid importing Drizzle's inferred
|
|
155
|
+
* insert type and to let tests build rows without a db client.
|
|
156
|
+
*/
|
|
157
|
+
export interface McpUsageMeterRow {
|
|
158
|
+
/** Synthetic primary key. Default: `crypto.randomUUID()`. */
|
|
159
|
+
id: string;
|
|
160
|
+
/** Tenant / account fk (NOT NULL on `usage_meters`). */
|
|
161
|
+
accountId: string;
|
|
162
|
+
/** Dot-notation meter name: mirrors `McpLogEvent.kind`. */
|
|
163
|
+
meterName: string;
|
|
164
|
+
/**
|
|
165
|
+
* Event count. v1 uses `1` per call (row-per-call pattern); billing
|
|
166
|
+
* aggregation rolls these up. A larger quantity makes sense only
|
|
167
|
+
* when the meter represents a volume (tokens, bytes) rather than
|
|
168
|
+
* a count of events.
|
|
169
|
+
*/
|
|
170
|
+
quantity: number;
|
|
171
|
+
/** Timestamp the metered event occurred. */
|
|
172
|
+
periodStart: Date;
|
|
173
|
+
/** Reserved for aggregation-mode rows; `null` for per-call rows. */
|
|
174
|
+
periodEnd: Date | null;
|
|
175
|
+
/**
|
|
176
|
+
* Origin label. Per the `usage_meters` CHECK constraint one of
|
|
177
|
+
* `'system' | 'user' | 'agent' | 'api'`. Defaults to `'agent'`
|
|
178
|
+
* since this sink consumes agent-runtime protocol events.
|
|
179
|
+
*/
|
|
180
|
+
source: 'system' | 'user' | 'agent' | 'api';
|
|
181
|
+
/** Unique key. Backs `uniqueIndex` on `usage_meters`. */
|
|
182
|
+
idempotencyKey: string;
|
|
183
|
+
/**
|
|
184
|
+
* Wall-clock duration of the protocol call in milliseconds, lifted
|
|
185
|
+
* directly from `event.duration_ms`. Optional in the row shape so
|
|
186
|
+
* pre-A.3 consumers writing into rows without a `duration_ms`
|
|
187
|
+
* column don't break — the sink populates it whenever the consumer's
|
|
188
|
+
* schema accepts it. A.3 of the post-v1 MCP arc.
|
|
189
|
+
*/
|
|
190
|
+
durationMs?: number;
|
|
191
|
+
/**
|
|
192
|
+
* `true` when the protocol call surfaced an error (`!event.success`).
|
|
193
|
+
* Optional for the same forward-compat reason as `durationMs`.
|
|
194
|
+
* The Usage tab on `/admin/mcp` aggregates these into success-rate
|
|
195
|
+
* counts per `meterName`. A.3.
|
|
196
|
+
*/
|
|
197
|
+
errored?: boolean;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Consumer-supplied row writer. Typically:
|
|
201
|
+
* `(row) => db.insert(usageMeters).values(row)`.
|
|
202
|
+
* Can be sync (throws on error) or async. The sink awaits thenable
|
|
203
|
+
* results and logs rejections at warn — write failures never propagate
|
|
204
|
+
* back into the MCP call that produced the event.
|
|
205
|
+
*/
|
|
206
|
+
export type McpUsageMeterWriter = (row: McpUsageMeterRow) => void | Promise<void>;
|
|
207
|
+
export interface CreateUsageMeterSinkOptions {
|
|
208
|
+
/**
|
|
209
|
+
* Tenant / account id required by the `usage_meters.accountId NOT
|
|
210
|
+
* NULL fk` constraint. Agent-side adapters are tenant-agnostic by
|
|
211
|
+
* design — consumer binds the correct account when constructing
|
|
212
|
+
* the sink, typically one sink per authenticated call path.
|
|
213
|
+
*/
|
|
214
|
+
accountId: string;
|
|
215
|
+
/**
|
|
216
|
+
* Row persistence callback. Structural so consumers can plug in
|
|
217
|
+
* raw Drizzle, a wrapped client, or a test double.
|
|
218
|
+
*/
|
|
219
|
+
write: McpUsageMeterWriter;
|
|
220
|
+
/**
|
|
221
|
+
* Origin label per `usage_meters.source` check constraint. Default
|
|
222
|
+
* `'agent'`.
|
|
223
|
+
*/
|
|
224
|
+
source?: McpUsageMeterRow['source'];
|
|
225
|
+
/**
|
|
226
|
+
* Custom idempotency-key generator. Default: fresh
|
|
227
|
+
* `crypto.randomUUID()` per event (safe; no retry coalescence).
|
|
228
|
+
* Supply a deterministic generator (e.g. `${requestId}:${e.kind}`)
|
|
229
|
+
* to collapse duplicate retries of one semantic call to a single
|
|
230
|
+
* row.
|
|
231
|
+
*/
|
|
232
|
+
idempotencyKey?: (event: McpLogEvent) => string;
|
|
233
|
+
/**
|
|
234
|
+
* Custom row-id generator. Default: fresh UUID. Provided as a hook
|
|
235
|
+
* for consumers integrating with an existing id scheme.
|
|
236
|
+
*/
|
|
237
|
+
id?: (event: McpLogEvent) => string;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build an `McpEventSink` that translates Stage 6.1 protocol log
|
|
241
|
+
* events into `usage_meters` insert rows (Stage 6.2).
|
|
242
|
+
*
|
|
243
|
+
* This is the agent-adapter-lane counterpart to the hypervisor's
|
|
244
|
+
* `setUsageMeterSink()` in `@revealui/mcp`. Both paths land rows in
|
|
245
|
+
* the same `usage_meters` table. The agent-adapter path covers the
|
|
246
|
+
* four boundaries the hypervisor doesn't own
|
|
247
|
+
* (`resource.list/read`, `prompt.list/get`, `sampling.create`,
|
|
248
|
+
* `elicitation.create`) plus the agent-side `tool.call` for
|
|
249
|
+
* `McpClient`-using agents.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* import { usageMeters } from '@revealui/db';
|
|
254
|
+
* import {
|
|
255
|
+
* createToolsFromMcpClient,
|
|
256
|
+
* createUsageMeterSink,
|
|
257
|
+
* } from '@revealui/ai';
|
|
258
|
+
*
|
|
259
|
+
* const tools = await createToolsFromMcpClient(client, {
|
|
260
|
+
* namespace: 'content',
|
|
261
|
+
* onEvent: createUsageMeterSink({
|
|
262
|
+
* accountId: session.accountId,
|
|
263
|
+
* write: (row) => db.insert(usageMeters).values(row),
|
|
264
|
+
* }),
|
|
265
|
+
* });
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export declare function createUsageMeterSink(options: CreateUsageMeterSinkOptions): McpEventSink;
|
|
269
|
+
export {};
|
|
270
|
+
//# sourceMappingURL=mcp-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-events.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,0DAA0D;AAC1D,UAAU,YAAY;IACpB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,IAAI,EAAE,eAAe,CAAC;IACtB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACxD,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACxD,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D,IAAI,EAAE,qBAAqB,CAAC;IAC5B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAA0B,SAAQ,YAAY;IAC7D,IAAI,EAAE,wBAAwB,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACxC,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GACnB,gBAAgB,GAChB,oBAAoB,GACpB,oBAAoB,GACpB,kBAAkB,GAClB,iBAAiB,GACjB,sBAAsB,GACtB,yBAAyB,CAAC;AAE9B;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAMxD,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,2BAAgC,GAAG,YAAY,CAe5F;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAUrF;AAMD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,WAAW,EAAE,IAAI,CAAC;IAClB,oEAAoE;IACpE,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB;;;;OAIG;IACH,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IAC5C,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAElF,MAAM,WAAW,2BAA2B;IAC1C;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,mBAAmB,CAAC;IAC3B;;;OAGG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC;IAChD;;;OAGG;IACH,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC;CACrC;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,YAAY,CA6BvF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol log channel — structured events per server-to-client call
|
|
3
|
+
* (Stage 6.1 of the MCP v1 plan).
|
|
4
|
+
*
|
|
5
|
+
* Stage 5 shipped the full agent-side MCP protocol surface
|
|
6
|
+
* (tools + resources + prompts + sampling + elicitation + progress +
|
|
7
|
+
* cancellation). Stage 6.1 adds observability: every adapter in
|
|
8
|
+
* `@revealui/ai/tools/mcp-*` grows an `onEvent?: McpEventSink` hook that
|
|
9
|
+
* fires once per protocol call with a structured summary. Consumers wire
|
|
10
|
+
* the sink to whatever aggregator they run — typically
|
|
11
|
+
* `@revealui/core/observability/logger` via `createCoreLoggerSink()`.
|
|
12
|
+
*
|
|
13
|
+
* Instrumentation lives at the agent-adapter layer by design. The
|
|
14
|
+
* hypervisor is a separate lane (Stage 6.2 lands `usage_meters` rows
|
|
15
|
+
* there, where `accountId` is in scope). Agent adapters are intentionally
|
|
16
|
+
* tenant-agnostic, so events carry no `accountId` field — the consumer
|
|
17
|
+
* adds tenant context in their sink wrapper if they need it for routing.
|
|
18
|
+
*
|
|
19
|
+
* PII note
|
|
20
|
+
* --------
|
|
21
|
+
* Errors surfaced by the server can legitimately contain user-identifying
|
|
22
|
+
* strings (e.g. "user 'alice@example.com' not found"). `error` is passed
|
|
23
|
+
* through to the sink verbatim. If your deployment's observability layer
|
|
24
|
+
* is subject to PII constraints, wrap the sink with a redactor
|
|
25
|
+
* (`redactLogContext` from `@revealui/security`) before passing it in.
|
|
26
|
+
*
|
|
27
|
+
* Params / args / content bodies are **not** included in events —
|
|
28
|
+
* observability is summary-grained by default to avoid accidentally
|
|
29
|
+
* logging credentials, prompts, or document contents.
|
|
30
|
+
*/
|
|
31
|
+
import { logger } from '@revealui/core/observability/logger';
|
|
32
|
+
/**
|
|
33
|
+
* Build an `McpEventSink` that fans events into the central log
|
|
34
|
+
* aggregator via `@revealui/core/observability/logger`. This is the
|
|
35
|
+
* recommended default for in-process consumers (admin app, API, agent
|
|
36
|
+
* orchestration). Out-of-process consumers (CLI, remote inspector)
|
|
37
|
+
* typically construct a custom sink that forwards to their own transport.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import {
|
|
42
|
+
* createToolsFromMcpClient,
|
|
43
|
+
* createCoreLoggerSink,
|
|
44
|
+
* } from '@revealui/ai';
|
|
45
|
+
*
|
|
46
|
+
* const onEvent = createCoreLoggerSink();
|
|
47
|
+
*
|
|
48
|
+
* const tools = await createToolsFromMcpClient(client, {
|
|
49
|
+
* namespace: 'my-server',
|
|
50
|
+
* onEvent,
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function createCoreLoggerSink(options = {}) {
|
|
55
|
+
const successLevel = options.successLevel ?? 'info';
|
|
56
|
+
return (event) => {
|
|
57
|
+
const { kind, ...rest } = event;
|
|
58
|
+
const payload = { event: kind, ...rest };
|
|
59
|
+
if (!event.success) {
|
|
60
|
+
logger.warn(`[mcp] ${kind}`, payload);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (successLevel === 'debug') {
|
|
64
|
+
logger.debug(`[mcp] ${kind}`, payload);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
logger.info(`[mcp] ${kind}`, payload);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Internal helper — shared across mcp-adapter / mcp-sampling / mcp-elicitation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* Invoke a sink, swallowing any thrown error. A misbehaving sink must
|
|
76
|
+
* never break the underlying MCP call — observability is best-effort.
|
|
77
|
+
* Logs the sink failure at warn so the regression is still visible.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function emitMcpEvent(sink, event) {
|
|
82
|
+
if (!sink)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
sink(event);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.warn('[mcp] event sink threw; continuing', {
|
|
89
|
+
event: event.kind,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Map an `McpLogEvent.kind` to a `usage_meters.meterName` string.
|
|
96
|
+
* One-to-one: the dot-notation kind IS the meter name.
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
const METER_NAMES = {
|
|
101
|
+
'mcp.tool.call': 'mcp.tool.call',
|
|
102
|
+
'mcp.resource.list': 'mcp.resource.list',
|
|
103
|
+
'mcp.resource.read': 'mcp.resource.read',
|
|
104
|
+
'mcp.prompt.list': 'mcp.prompt.list',
|
|
105
|
+
'mcp.prompt.get': 'mcp.prompt.get',
|
|
106
|
+
'mcp.sampling.create': 'mcp.sampling.create',
|
|
107
|
+
'mcp.elicitation.create': 'mcp.elicitation.create',
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Build an `McpEventSink` that translates Stage 6.1 protocol log
|
|
111
|
+
* events into `usage_meters` insert rows (Stage 6.2).
|
|
112
|
+
*
|
|
113
|
+
* This is the agent-adapter-lane counterpart to the hypervisor's
|
|
114
|
+
* `setUsageMeterSink()` in `@revealui/mcp`. Both paths land rows in
|
|
115
|
+
* the same `usage_meters` table. The agent-adapter path covers the
|
|
116
|
+
* four boundaries the hypervisor doesn't own
|
|
117
|
+
* (`resource.list/read`, `prompt.list/get`, `sampling.create`,
|
|
118
|
+
* `elicitation.create`) plus the agent-side `tool.call` for
|
|
119
|
+
* `McpClient`-using agents.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* import { usageMeters } from '@revealui/db';
|
|
124
|
+
* import {
|
|
125
|
+
* createToolsFromMcpClient,
|
|
126
|
+
* createUsageMeterSink,
|
|
127
|
+
* } from '@revealui/ai';
|
|
128
|
+
*
|
|
129
|
+
* const tools = await createToolsFromMcpClient(client, {
|
|
130
|
+
* namespace: 'content',
|
|
131
|
+
* onEvent: createUsageMeterSink({
|
|
132
|
+
* accountId: session.accountId,
|
|
133
|
+
* write: (row) => db.insert(usageMeters).values(row),
|
|
134
|
+
* }),
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function createUsageMeterSink(options) {
|
|
139
|
+
const source = options.source ?? 'agent';
|
|
140
|
+
const genId = options.id ?? (() => crypto.randomUUID());
|
|
141
|
+
const genKey = options.idempotencyKey ?? (() => crypto.randomUUID());
|
|
142
|
+
return (event) => {
|
|
143
|
+
const row = {
|
|
144
|
+
id: genId(event),
|
|
145
|
+
accountId: options.accountId,
|
|
146
|
+
meterName: METER_NAMES[event.kind],
|
|
147
|
+
quantity: 1,
|
|
148
|
+
periodStart: new Date(),
|
|
149
|
+
periodEnd: null,
|
|
150
|
+
source,
|
|
151
|
+
idempotencyKey: genKey(event),
|
|
152
|
+
durationMs: event.duration_ms,
|
|
153
|
+
errored: !event.success,
|
|
154
|
+
};
|
|
155
|
+
const result = options.write(row);
|
|
156
|
+
if (result && typeof result.catch === 'function') {
|
|
157
|
+
result.catch((error) => {
|
|
158
|
+
logger.warn('[mcp] usage meter write rejected; continuing', {
|
|
159
|
+
event: event.kind,
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|