@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
|
@@ -1,9 +1,418 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Tool Adapter
|
|
3
3
|
*
|
|
4
|
-
* Bridges MCP (Model Context Protocol) servers to the tool system
|
|
4
|
+
* Bridges MCP (Model Context Protocol) servers to the tool system.
|
|
5
|
+
*
|
|
6
|
+
* Two paths:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Standard MCP client** (Stage 5.1a, preferred) — consumers construct an
|
|
9
|
+
* `McpClient` from `@revealui/mcp/client` against stdio or Streamable HTTP,
|
|
10
|
+
* and pass it to `createToolsFromMcpClient()` (or to `AgentRuntime` via
|
|
11
|
+
* `mcpClients`). This is the full-protocol path: future stages will extend
|
|
12
|
+
* this to resources, prompts, sampling, elicitation, etc.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Hypervisor** (legacy, pre-5.1a) — consumers pass an `MCPHypervisor` (or
|
|
15
|
+
* any `MCPToolSource`) to `discoverMCPTools()`. Kept for backwards compat;
|
|
16
|
+
* deprecated in favor of path (1). The hypervisor still owns server-side
|
|
17
|
+
* subprocess + tenant-scoping concerns and isn't going away.
|
|
18
|
+
*
|
|
19
|
+
* Both paths are deliberately **structurally typed** — `@revealui/ai` has no
|
|
20
|
+
* runtime dependency on `@revealui/mcp`. Consumers satisfy the shapes
|
|
21
|
+
* `McpClientLike` / `MCPToolSource` with whichever client they construct.
|
|
5
22
|
*/
|
|
6
23
|
import { z } from 'zod/v4';
|
|
24
|
+
import { emitMcpEvent } from './mcp-events.js';
|
|
25
|
+
/**
|
|
26
|
+
* Connect the agent's tool registry to a standard MCP client. Lists tools
|
|
27
|
+
* from the MCP server and returns agent-side `Tool` instances that dispatch
|
|
28
|
+
* calls back through the client. Safe to call multiple times per server —
|
|
29
|
+
* each invocation re-reads the current tool list.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { McpClient } from '@revealui/mcp/client';
|
|
34
|
+
* import { createToolsFromMcpClient } from '@revealui/ai';
|
|
35
|
+
*
|
|
36
|
+
* const client = new McpClient({
|
|
37
|
+
* clientInfo: { name: 'my-agent', version: '1.0.0' },
|
|
38
|
+
* transport: { kind: 'streamable-http', url: 'https://example.com/mcp' },
|
|
39
|
+
* });
|
|
40
|
+
* await client.connect();
|
|
41
|
+
*
|
|
42
|
+
* const tools = await createToolsFromMcpClient(client, {
|
|
43
|
+
* namespace: 'example-server',
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* agent.tools.push(...tools);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export async function createToolsFromMcpClient(client, options) {
|
|
50
|
+
if (!(options.namespace && /^[a-zA-Z0-9_-]+$/.test(options.namespace))) {
|
|
51
|
+
throw new Error(`createToolsFromMcpClient: namespace must be a non-empty string of [a-zA-Z0-9_-], got ${JSON.stringify(options.namespace)}`);
|
|
52
|
+
}
|
|
53
|
+
const category = options.category ?? 'mcp';
|
|
54
|
+
const includeTools = options.include?.tools !== false;
|
|
55
|
+
const includeResources = options.include?.resources !== false;
|
|
56
|
+
const includePrompts = options.include?.prompts !== false;
|
|
57
|
+
const ctx = {
|
|
58
|
+
namespace: options.namespace,
|
|
59
|
+
category,
|
|
60
|
+
...(options.onProgress !== undefined ? { onProgress: options.onProgress } : {}),
|
|
61
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
62
|
+
...(options.onEvent !== undefined ? { onEvent: options.onEvent } : {}),
|
|
63
|
+
};
|
|
64
|
+
const tools = [];
|
|
65
|
+
// --- Server tools (Stage 5.1a) -----------------------------------------
|
|
66
|
+
if (includeTools) {
|
|
67
|
+
const descriptors = await client.listTools();
|
|
68
|
+
for (const descriptor of descriptors) {
|
|
69
|
+
tools.push(buildServerTool(client, descriptor, ctx));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// --- Resource meta-tools (Stage 5.1b) ----------------------------------
|
|
73
|
+
if (includeResources && client.listResources && client.readResource) {
|
|
74
|
+
tools.push(buildListResourcesTool(client, ctx));
|
|
75
|
+
tools.push(buildReadResourceTool(client, ctx));
|
|
76
|
+
}
|
|
77
|
+
// --- Prompt meta-tools (Stage 5.1b) ------------------------------------
|
|
78
|
+
if (includePrompts && client.listPrompts && client.getPrompt) {
|
|
79
|
+
tools.push(buildListPromptsTool(client, ctx));
|
|
80
|
+
tools.push(buildGetPromptTool(client, ctx));
|
|
81
|
+
}
|
|
82
|
+
return tools;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the request-options object passed to `client.callTool()` /
|
|
86
|
+
* `readResource()` / etc. Wraps the ctx's `onProgress` with the
|
|
87
|
+
* specific tool name + namespace so every emitted event is attributable.
|
|
88
|
+
*/
|
|
89
|
+
function buildRequestOptions(ctx, toolName) {
|
|
90
|
+
const opts = {};
|
|
91
|
+
if (ctx.onProgress) {
|
|
92
|
+
const emit = ctx.onProgress;
|
|
93
|
+
opts.onProgress = (progress) => emit({ namespace: ctx.namespace, toolName, progress });
|
|
94
|
+
}
|
|
95
|
+
if (ctx.signal)
|
|
96
|
+
opts.signal = ctx.signal;
|
|
97
|
+
return opts.onProgress || opts.signal ? opts : undefined;
|
|
98
|
+
}
|
|
99
|
+
function buildServerTool(client, descriptor, ctx) {
|
|
100
|
+
const zodSchema = jsonSchemaObjectToZod(descriptor.inputSchema);
|
|
101
|
+
const namespacedName = `mcp_${ctx.namespace}__${descriptor.name}`;
|
|
102
|
+
return {
|
|
103
|
+
name: namespacedName,
|
|
104
|
+
label: descriptor.name,
|
|
105
|
+
description: descriptor.description ?? `${ctx.namespace}: ${descriptor.name}`,
|
|
106
|
+
parameters: zodSchema,
|
|
107
|
+
async execute(params) {
|
|
108
|
+
const validated = zodSchema.parse(params);
|
|
109
|
+
const started = Date.now();
|
|
110
|
+
try {
|
|
111
|
+
const result = await client.callTool(descriptor.name, validated, buildRequestOptions(ctx, descriptor.name));
|
|
112
|
+
if (result.isError) {
|
|
113
|
+
const errorText = extractErrorText(result);
|
|
114
|
+
emitMcpEvent(ctx.onEvent, {
|
|
115
|
+
kind: 'mcp.tool.call',
|
|
116
|
+
namespace: ctx.namespace,
|
|
117
|
+
toolName: descriptor.name,
|
|
118
|
+
duration_ms: Date.now() - started,
|
|
119
|
+
success: false,
|
|
120
|
+
error: errorText,
|
|
121
|
+
});
|
|
122
|
+
return { success: false, error: errorText };
|
|
123
|
+
}
|
|
124
|
+
emitMcpEvent(ctx.onEvent, {
|
|
125
|
+
kind: 'mcp.tool.call',
|
|
126
|
+
namespace: ctx.namespace,
|
|
127
|
+
toolName: descriptor.name,
|
|
128
|
+
duration_ms: Date.now() - started,
|
|
129
|
+
success: true,
|
|
130
|
+
});
|
|
131
|
+
const payload = result.structuredContent ?? result.content;
|
|
132
|
+
return { success: true, data: serializeMCPResult(payload) };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
emitMcpEvent(ctx.onEvent, {
|
|
137
|
+
kind: 'mcp.tool.call',
|
|
138
|
+
namespace: ctx.namespace,
|
|
139
|
+
toolName: descriptor.name,
|
|
140
|
+
duration_ms: Date.now() - started,
|
|
141
|
+
success: false,
|
|
142
|
+
error: message,
|
|
143
|
+
});
|
|
144
|
+
return { success: false, error: message };
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
getMetadata() {
|
|
148
|
+
return { category: ctx.category, version: '1.0.0', mcpNamespace: ctx.namespace };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function buildListResourcesTool(client, ctx) {
|
|
153
|
+
return {
|
|
154
|
+
name: `mcp_${ctx.namespace}__list_resources`,
|
|
155
|
+
label: 'list_resources',
|
|
156
|
+
description: `List resources exposed by the ${ctx.namespace} MCP server. Returns an array of { uri, name, description?, mimeType? }.`,
|
|
157
|
+
parameters: z.object({}),
|
|
158
|
+
async execute() {
|
|
159
|
+
const started = Date.now();
|
|
160
|
+
try {
|
|
161
|
+
const resources = (await client.listResources?.(buildRequestOptions(ctx, 'list_resources'))) ?? [];
|
|
162
|
+
emitMcpEvent(ctx.onEvent, {
|
|
163
|
+
kind: 'mcp.resource.list',
|
|
164
|
+
namespace: ctx.namespace,
|
|
165
|
+
duration_ms: Date.now() - started,
|
|
166
|
+
success: true,
|
|
167
|
+
resourceCount: resources.length,
|
|
168
|
+
});
|
|
169
|
+
return { success: true, data: serializeMCPResult(resources) };
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
emitMcpEvent(ctx.onEvent, {
|
|
174
|
+
kind: 'mcp.resource.list',
|
|
175
|
+
namespace: ctx.namespace,
|
|
176
|
+
duration_ms: Date.now() - started,
|
|
177
|
+
success: false,
|
|
178
|
+
error: message,
|
|
179
|
+
});
|
|
180
|
+
return { success: false, error: message };
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
getMetadata() {
|
|
184
|
+
return {
|
|
185
|
+
category: ctx.category,
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
mcpNamespace: ctx.namespace,
|
|
188
|
+
kind: 'resources',
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function buildReadResourceTool(client, ctx) {
|
|
194
|
+
const paramsSchema = z.object({
|
|
195
|
+
uri: z.string().min(1).describe('Resource URI to read (e.g. revealui-content://posts/abc)'),
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
name: `mcp_${ctx.namespace}__read_resource`,
|
|
199
|
+
label: 'read_resource',
|
|
200
|
+
description: `Read a resource by URI from the ${ctx.namespace} MCP server. Returns the resource contents (text parts flattened to a joined string when possible).`,
|
|
201
|
+
parameters: paramsSchema,
|
|
202
|
+
async execute(params) {
|
|
203
|
+
const { uri } = paramsSchema.parse(params);
|
|
204
|
+
const started = Date.now();
|
|
205
|
+
try {
|
|
206
|
+
const contents = (await client.readResource?.(uri, buildRequestOptions(ctx, 'read_resource'))) ?? [];
|
|
207
|
+
emitMcpEvent(ctx.onEvent, {
|
|
208
|
+
kind: 'mcp.resource.read',
|
|
209
|
+
namespace: ctx.namespace,
|
|
210
|
+
uri,
|
|
211
|
+
duration_ms: Date.now() - started,
|
|
212
|
+
success: true,
|
|
213
|
+
});
|
|
214
|
+
const joinedText = flattenResourceText(contents);
|
|
215
|
+
const base = {
|
|
216
|
+
success: true,
|
|
217
|
+
data: serializeMCPResult(contents),
|
|
218
|
+
};
|
|
219
|
+
return joinedText !== undefined ? { ...base, content: joinedText } : base;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
emitMcpEvent(ctx.onEvent, {
|
|
224
|
+
kind: 'mcp.resource.read',
|
|
225
|
+
namespace: ctx.namespace,
|
|
226
|
+
uri,
|
|
227
|
+
duration_ms: Date.now() - started,
|
|
228
|
+
success: false,
|
|
229
|
+
error: message,
|
|
230
|
+
});
|
|
231
|
+
return { success: false, error: message };
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
getMetadata() {
|
|
235
|
+
return {
|
|
236
|
+
category: ctx.category,
|
|
237
|
+
version: '1.0.0',
|
|
238
|
+
mcpNamespace: ctx.namespace,
|
|
239
|
+
kind: 'resources',
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function buildListPromptsTool(client, ctx) {
|
|
245
|
+
return {
|
|
246
|
+
name: `mcp_${ctx.namespace}__list_prompts`,
|
|
247
|
+
label: 'list_prompts',
|
|
248
|
+
description: `List prompts exposed by the ${ctx.namespace} MCP server. Returns an array of { name, description?, arguments? }.`,
|
|
249
|
+
parameters: z.object({}),
|
|
250
|
+
async execute() {
|
|
251
|
+
const started = Date.now();
|
|
252
|
+
try {
|
|
253
|
+
const prompts = (await client.listPrompts?.(buildRequestOptions(ctx, 'list_prompts'))) ?? [];
|
|
254
|
+
emitMcpEvent(ctx.onEvent, {
|
|
255
|
+
kind: 'mcp.prompt.list',
|
|
256
|
+
namespace: ctx.namespace,
|
|
257
|
+
duration_ms: Date.now() - started,
|
|
258
|
+
success: true,
|
|
259
|
+
promptCount: prompts.length,
|
|
260
|
+
});
|
|
261
|
+
return { success: true, data: serializeMCPResult(prompts) };
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
emitMcpEvent(ctx.onEvent, {
|
|
266
|
+
kind: 'mcp.prompt.list',
|
|
267
|
+
namespace: ctx.namespace,
|
|
268
|
+
duration_ms: Date.now() - started,
|
|
269
|
+
success: false,
|
|
270
|
+
error: message,
|
|
271
|
+
});
|
|
272
|
+
return { success: false, error: message };
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
getMetadata() {
|
|
276
|
+
return {
|
|
277
|
+
category: ctx.category,
|
|
278
|
+
version: '1.0.0',
|
|
279
|
+
mcpNamespace: ctx.namespace,
|
|
280
|
+
kind: 'prompts',
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function buildGetPromptTool(client, ctx) {
|
|
286
|
+
const paramsSchema = z.object({
|
|
287
|
+
name: z.string().min(1).describe('Prompt name to retrieve'),
|
|
288
|
+
args: z
|
|
289
|
+
.record(z.string(), z.string())
|
|
290
|
+
.optional()
|
|
291
|
+
.describe('Prompt arguments as a string-valued map (per MCP spec)'),
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
name: `mcp_${ctx.namespace}__get_prompt`,
|
|
295
|
+
label: 'get_prompt',
|
|
296
|
+
description: `Get a resolved prompt from the ${ctx.namespace} MCP server. Returns { description?, messages } — messages is an array of { role, content }.`,
|
|
297
|
+
parameters: paramsSchema,
|
|
298
|
+
async execute(params) {
|
|
299
|
+
const { name, args } = paramsSchema.parse(params);
|
|
300
|
+
const started = Date.now();
|
|
301
|
+
try {
|
|
302
|
+
const result = await client.getPrompt?.(name, args, buildRequestOptions(ctx, 'get_prompt'));
|
|
303
|
+
if (!result) {
|
|
304
|
+
const msg = 'client does not implement getPrompt';
|
|
305
|
+
emitMcpEvent(ctx.onEvent, {
|
|
306
|
+
kind: 'mcp.prompt.get',
|
|
307
|
+
namespace: ctx.namespace,
|
|
308
|
+
promptName: name,
|
|
309
|
+
duration_ms: Date.now() - started,
|
|
310
|
+
success: false,
|
|
311
|
+
error: msg,
|
|
312
|
+
});
|
|
313
|
+
return { success: false, error: msg };
|
|
314
|
+
}
|
|
315
|
+
emitMcpEvent(ctx.onEvent, {
|
|
316
|
+
kind: 'mcp.prompt.get',
|
|
317
|
+
namespace: ctx.namespace,
|
|
318
|
+
promptName: name,
|
|
319
|
+
duration_ms: Date.now() - started,
|
|
320
|
+
success: true,
|
|
321
|
+
});
|
|
322
|
+
const joinedText = flattenPromptMessages(result.messages);
|
|
323
|
+
const base = {
|
|
324
|
+
success: true,
|
|
325
|
+
data: serializeMCPResult(result),
|
|
326
|
+
};
|
|
327
|
+
return joinedText !== undefined ? { ...base, content: joinedText } : base;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
331
|
+
emitMcpEvent(ctx.onEvent, {
|
|
332
|
+
kind: 'mcp.prompt.get',
|
|
333
|
+
namespace: ctx.namespace,
|
|
334
|
+
promptName: name,
|
|
335
|
+
duration_ms: Date.now() - started,
|
|
336
|
+
success: false,
|
|
337
|
+
error: message,
|
|
338
|
+
});
|
|
339
|
+
return { success: false, error: message };
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
getMetadata() {
|
|
343
|
+
return {
|
|
344
|
+
category: ctx.category,
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
mcpNamespace: ctx.namespace,
|
|
347
|
+
kind: 'prompts',
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Collapse an array of resource contents to a single text string when every
|
|
354
|
+
* part carries text. Returns `undefined` if any part is binary (blob) or has
|
|
355
|
+
* no text, signaling the caller that a token-efficient summary isn't
|
|
356
|
+
* available — in which case the full `data` array still carries everything.
|
|
357
|
+
*/
|
|
358
|
+
function flattenResourceText(contents) {
|
|
359
|
+
if (contents.length === 0)
|
|
360
|
+
return undefined;
|
|
361
|
+
const parts = [];
|
|
362
|
+
for (const part of contents) {
|
|
363
|
+
if (typeof part.text === 'string') {
|
|
364
|
+
parts.push(part.text);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return parts.join('\n\n');
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Collapse a prompt's message array to a single text summary in
|
|
374
|
+
* `<role>: <text>` format. Returns `undefined` when any message has a
|
|
375
|
+
* non-text content shape (image, resource reference, …).
|
|
376
|
+
*/
|
|
377
|
+
function flattenPromptMessages(messages) {
|
|
378
|
+
if (messages.length === 0)
|
|
379
|
+
return undefined;
|
|
380
|
+
const lines = [];
|
|
381
|
+
for (const msg of messages) {
|
|
382
|
+
const text = extractMessageText(msg.content);
|
|
383
|
+
if (text === undefined)
|
|
384
|
+
return undefined;
|
|
385
|
+
lines.push(`${msg.role}: ${text}`);
|
|
386
|
+
}
|
|
387
|
+
return lines.join('\n\n');
|
|
388
|
+
}
|
|
389
|
+
function extractMessageText(content) {
|
|
390
|
+
if (typeof content === 'string')
|
|
391
|
+
return content;
|
|
392
|
+
if (content && typeof content === 'object') {
|
|
393
|
+
const c = content;
|
|
394
|
+
if (c.type === 'text' && typeof c.text === 'string')
|
|
395
|
+
return c.text;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Extract a human-readable error string from an MCP `CallToolResult` that
|
|
401
|
+
* came back with `isError: true`. Servers put the error detail in `content`
|
|
402
|
+
* per spec; we concatenate any text parts.
|
|
403
|
+
*/
|
|
404
|
+
function extractErrorText(result) {
|
|
405
|
+
const texts = [];
|
|
406
|
+
for (const part of result.content) {
|
|
407
|
+
if (part && typeof part === 'object') {
|
|
408
|
+
const p = part;
|
|
409
|
+
if (p.type === 'text' && typeof p.text === 'string') {
|
|
410
|
+
texts.push(p.text);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return texts.length > 0 ? texts.join('\n') : 'Tool reported error (no detail)';
|
|
415
|
+
}
|
|
7
416
|
/**
|
|
8
417
|
* Create a Tool from an MCP tool definition
|
|
9
418
|
*/
|
|
@@ -108,6 +517,30 @@ export function discoverMCPTools(source) {
|
|
|
108
517
|
return agentTool;
|
|
109
518
|
});
|
|
110
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Narrow an untyped `inputSchema` from a spec `Tool` into the shape
|
|
522
|
+
* `jsonSchemaToZod` understands, then delegate. Falls back to a permissive
|
|
523
|
+
* `z.object({})` when the input is malformed so tool discovery never hard-fails
|
|
524
|
+
* on a misbehaving server.
|
|
525
|
+
*/
|
|
526
|
+
function jsonSchemaObjectToZod(raw) {
|
|
527
|
+
if (!(raw && typeof raw === 'object'))
|
|
528
|
+
return z.object({});
|
|
529
|
+
const s = raw;
|
|
530
|
+
if (s.type !== 'object')
|
|
531
|
+
return z.object({});
|
|
532
|
+
const properties = s.properties && typeof s.properties === 'object'
|
|
533
|
+
? s.properties
|
|
534
|
+
: undefined;
|
|
535
|
+
const required = Array.isArray(s.required)
|
|
536
|
+
? s.required.filter((r) => typeof r === 'string')
|
|
537
|
+
: undefined;
|
|
538
|
+
return jsonSchemaToZod({
|
|
539
|
+
type: 'object',
|
|
540
|
+
...(properties !== undefined ? { properties } : {}),
|
|
541
|
+
...(required !== undefined ? { required } : {}),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
111
544
|
/**
|
|
112
545
|
* Convert JSON Schema to Zod schema (simplified version)
|
|
113
546
|
* For production, use a proper library like json-schema-to-zod
|
|
@@ -0,0 +1,128 @@
|
|
|
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 { type McpEventSink } from './mcp-events.js';
|
|
53
|
+
/** Parameters of an `elicitation/create` request (spec-shaped subset). */
|
|
54
|
+
export interface McpElicitRequestParams {
|
|
55
|
+
/**
|
|
56
|
+
* Elicitation mode. Currently the spec defines `'form'` (inline form
|
|
57
|
+
* fields) as the only named value; future modes (e.g. `'url'` for
|
|
58
|
+
* out-of-band consent) may appear. Undefined = `'form'`.
|
|
59
|
+
*/
|
|
60
|
+
mode?: string;
|
|
61
|
+
/** User-facing prompt message. */
|
|
62
|
+
message: string;
|
|
63
|
+
/** JSON Schema describing the fields to collect. */
|
|
64
|
+
requestedSchema: {
|
|
65
|
+
type: 'object';
|
|
66
|
+
properties: Record<string, unknown>;
|
|
67
|
+
required?: ReadonlyArray<string>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Allowed values for `ElicitResult.content` per the MCP spec. */
|
|
71
|
+
export type McpElicitContentValue = string | number | boolean | ReadonlyArray<string>;
|
|
72
|
+
/** Result of an `elicitation/create` request (spec-shaped subset). */
|
|
73
|
+
export interface McpElicitResult {
|
|
74
|
+
action: 'accept' | 'decline' | 'cancel';
|
|
75
|
+
content?: Record<string, McpElicitContentValue>;
|
|
76
|
+
}
|
|
77
|
+
/** Structural shape of the handler — matches `ElicitationHandler` from `@revealui/mcp/client`. */
|
|
78
|
+
export type McpElicitationHandler = (params: McpElicitRequestParams) => Promise<McpElicitResult>;
|
|
79
|
+
export interface CreateElicitationHandlerOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Consumer-supplied async callback that renders UI and collects the
|
|
82
|
+
* response. Receives the spec-shaped request params; returns a
|
|
83
|
+
* spec-shaped result. The factory handles URL-mode auto-decline,
|
|
84
|
+
* timeouts, and error-to-cancel mapping around this callback.
|
|
85
|
+
*/
|
|
86
|
+
onElicit: (params: McpElicitRequestParams) => Promise<McpElicitResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Auto-decline deadline in milliseconds. When set and no response
|
|
89
|
+
* arrives within the window, the handler returns `{ action: 'cancel' }`
|
|
90
|
+
* (not `'decline'` — the timeout wasn't a user decision). Set to
|
|
91
|
+
* `undefined` / omit to wait indefinitely.
|
|
92
|
+
*/
|
|
93
|
+
timeoutMs?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Accept `mode: 'url'` elicitation requests (out-of-band consent via
|
|
96
|
+
* a separate browser tab). Default `false` — URL-mode requests are
|
|
97
|
+
* auto-declined for safety. Flip to `true` only if the consumer's
|
|
98
|
+
* UI renders a verifiable domain-check + explicit user approval.
|
|
99
|
+
*/
|
|
100
|
+
allowUrlMode?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Observability hook. Fires before `onElicit` is invoked with the
|
|
103
|
+
* message + field count. Useful for audit trails.
|
|
104
|
+
*/
|
|
105
|
+
onElicitationRequest?: (info: {
|
|
106
|
+
message: string;
|
|
107
|
+
fieldCount: number;
|
|
108
|
+
mode?: string;
|
|
109
|
+
}) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Protocol-level observability sink (Stage 6.1). Fires once per
|
|
112
|
+
* `elicitation/create` call (after the handler decides — whether
|
|
113
|
+
* the user accepted, declined, cancelled, url-mode auto-declined,
|
|
114
|
+
* or the timeout fired) with `{ kind: 'mcp.elicitation.create',
|
|
115
|
+
* action, fieldCount, mode?, duration_ms, success }`. URL-mode
|
|
116
|
+
* auto-decline + timeout-cancel both emit `success: true` — they
|
|
117
|
+
* are legitimate handler outcomes, not failures. Only thrown errors
|
|
118
|
+
* report `success: false`.
|
|
119
|
+
*/
|
|
120
|
+
onEvent?: McpEventSink;
|
|
121
|
+
/**
|
|
122
|
+
* Optional server identifier included in emitted events. Leave unset
|
|
123
|
+
* when the handler is shared across multiple servers.
|
|
124
|
+
*/
|
|
125
|
+
namespace?: string;
|
|
126
|
+
}
|
|
127
|
+
export declare function createElicitationHandler(options: CreateElicitationHandlerOptions): McpElicitationHandler;
|
|
128
|
+
//# sourceMappingURL=mcp-elicitation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-elicitation.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-elicitation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAGH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAMlE,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,eAAe,EAAE;QACf,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;KAClC,CAAC;CACH;AAED,kEAAkE;AAClE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;AAEtF,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;CACjD;AAED,kGAAkG;AAClG,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAMjG,MAAM,WAAW,+BAA+B;IAC9C;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACvE;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,+BAA+B,GACvC,qBAAqB,CAiEvB"}
|