@questpie/mcp 3.28.10 → 3.29.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 +42 -6
- package/dist/{create-server-DvumhdI_.mjs → create-server-DzTo-3Wy.mjs} +373 -60
- package/dist/{index-C3gm6ar5.d.mts → index-CLEEqMqK.d.mts} +2 -0
- package/dist/index.d.mts +43 -3
- package/dist/index.mjs +5 -5
- package/dist/{mcp-uf7du4OS.mjs → mcp-C55EQ-yd.mjs} +3 -2
- package/dist/modules/mcp.d.mts +1 -1
- package/dist/modules/mcp.mjs +1 -1
- package/dist/{plugin-BZQe1pji.mjs → plugin-BgwWI9S8.mjs} +20 -9
- package/dist/plugin.mjs +1 -1
- package/dist/{stdio-HmbmENQN.mjs → stdio-Bq7nzR79.mjs} +1 -1
- package/dist/{stdio-C3663EKG.d.mts → stdio-C2V13-vP.d.mts} +48 -2
- package/dist/stdio.d.mts +1 -1
- package/dist/stdio.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -94,6 +94,42 @@ export default mcpTool("publish-summary", {
|
|
|
94
94
|
});
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## Prompts
|
|
98
|
+
|
|
99
|
+
A prompt provider answers `prompts/list` and `prompts/get` per request, so the
|
|
100
|
+
prompts a caller sees can depend on who they are. Clients such as Claude Code
|
|
101
|
+
show them as slash commands. Prompts do not add to the `tools/list` catalogue.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// mcp-prompts/playbooks.ts
|
|
105
|
+
import { mcpPrompts } from "@questpie/mcp";
|
|
106
|
+
|
|
107
|
+
export default mcpPrompts("playbooks", {
|
|
108
|
+
access: ({ session }) => !!session,
|
|
109
|
+
scopes: "playbooks:read",
|
|
110
|
+
list: async ({ ctx }) =>
|
|
111
|
+
(await ctx.collections.playbooks.find({})).docs.map((doc) => ({
|
|
112
|
+
name: doc.slug,
|
|
113
|
+
description: doc.summary,
|
|
114
|
+
})),
|
|
115
|
+
get: async ({ ctx, name }) => {
|
|
116
|
+
const doc = await ctx.collections.playbooks.findOne({
|
|
117
|
+
where: { slug: name },
|
|
118
|
+
});
|
|
119
|
+
if (!doc) return null;
|
|
120
|
+
return {
|
|
121
|
+
messages: [{ role: "user", content: { type: "text", text: doc.body } }],
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`access` and `scopes` are required and gate the whole provider, exactly like a
|
|
128
|
+
custom tool. `get` returns `null` for a name the caller cannot use; the client
|
|
129
|
+
receives the same not-found error as for an unknown name. The server advertises
|
|
130
|
+
the `prompts` capability only when at least one provider is released. Prompts
|
|
131
|
+
are never served to remote workloads.
|
|
132
|
+
|
|
97
133
|
## Remote Workloads
|
|
98
134
|
|
|
99
135
|
Remote workload execution uses a separate public factory.
|
|
@@ -201,12 +237,12 @@ output, credentials, authorization envelopes, or workload attribution.
|
|
|
201
237
|
|
|
202
238
|
## Exports
|
|
203
239
|
|
|
204
|
-
| Entry Point | Purpose
|
|
205
|
-
| --------------------------- |
|
|
206
|
-
| `@questpie/mcp` | Config, plugin, server, tool
|
|
207
|
-
| `@questpie/mcp/modules/mcp` | `mcpModule` for `modules.ts`
|
|
208
|
-
| `@questpie/mcp/plugin` | Codegen plugin export
|
|
209
|
-
| `@questpie/mcp/stdio` | Stdio server entry
|
|
240
|
+
| Entry Point | Purpose |
|
|
241
|
+
| --------------------------- | ------------------------------------------------- |
|
|
242
|
+
| `@questpie/mcp` | Config, plugin, server, tool and prompt factories |
|
|
243
|
+
| `@questpie/mcp/modules/mcp` | `mcpModule` for `modules.ts` |
|
|
244
|
+
| `@questpie/mcp/plugin` | Codegen plugin export |
|
|
245
|
+
| `@questpie/mcp/stdio` | Stdio server entry |
|
|
210
246
|
|
|
211
247
|
## License
|
|
212
248
|
|
|
@@ -1,63 +1,11 @@
|
|
|
1
1
|
import { createAdapterContext, createContextFactory, defaultOperationScope, evaluateRouteAccess, executeJsonRoute, extractAppServices, introspectCollection, introspectGlobal, introspectRoutes, isJsonRoute, umbrellaScopeFor } from "questpie";
|
|
2
|
+
import { ErrorCode, GetPromptRequestSchema, GetPromptResultSchema, ListPromptsRequestSchema, ListToolsRequestSchema, McpError, PromptSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { types } from "node:util";
|
|
4
5
|
import { snapshotBoundedBrokerValue } from "questpie/executor";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
import { ListToolsRequestSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
|
|
9
|
-
//#region src/server/mcp-tool.ts
|
|
10
|
-
var McpToolBuilder = class {
|
|
11
|
-
constructor(name, config) {
|
|
12
|
-
this.name = name;
|
|
13
|
-
this.config = config;
|
|
14
|
-
}
|
|
15
|
-
handler(handler) {
|
|
16
|
-
return Object.freeze({
|
|
17
|
-
__brand: "mcpTool",
|
|
18
|
-
name: this.name,
|
|
19
|
-
config: this.config,
|
|
20
|
-
handler
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Define a custom MCP tool, contributed by a module the same way collections and
|
|
26
|
-
* routes are (via `ModuleDefinition.mcpTools`). `mcpTool(name, config).handler(fn)`
|
|
27
|
-
* returns a frozen, branded {@link McpToolDefinition}; the CRUD/route tools the
|
|
28
|
-
* framework auto-generates are the same shape.
|
|
29
|
-
*
|
|
30
|
-
* `config` mirrors the MCP tool contract: `inputSchema`/`outputSchema` (Zod),
|
|
31
|
-
* `title`/`description`/`annotations` for discovery, plus the two QUESTPIE
|
|
32
|
-
* authorization hooks that compose exactly like the built-in tools:
|
|
33
|
-
* - `access` — required explicit opt-in and an {@link McpAccessRule} run against the caller's transport /
|
|
34
|
-
* accessMode / session (and, for an OAuth caller, its scopes). Same RBAC-style
|
|
35
|
-
* gate the CRUD tools use.
|
|
36
|
-
* - `scopes` — the OAuth scopes an `oauth` caller must hold ({@link McpToolConfig.scopes}).
|
|
37
|
-
* Custom tools have no default scope mapping (unlike CRUD, there is no
|
|
38
|
-
* resource/operation to derive one from), so it is required; use `false` for
|
|
39
|
-
* an explicit no-OAuth-scope policy. The shipped scope gate ({@link scopeGateAllows}) enforces this at both
|
|
40
|
-
* `tools/list` (hidden) and `tools/call` (denied); `system`/`user` callers
|
|
41
|
-
* carry no scopes and skip it, so the effective bound is always `scopes ∩ RBAC`.
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```ts
|
|
45
|
-
* export const ping = mcpTool("ops.ping", {
|
|
46
|
-
* access: ({ session }) => !!session,
|
|
47
|
-
* description: "Health probe",
|
|
48
|
-
* inputSchema: z.object({}),
|
|
49
|
-
* scopes: "routes:ops/ping:invoke", // only OAuth callers holding this scope
|
|
50
|
-
* }).handler(async () => ({ content: [{ type: "text", text: "pong" }] }));
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
function mcpTool(name, config) {
|
|
54
|
-
return new McpToolBuilder(name, config);
|
|
55
|
-
}
|
|
56
|
-
function isMcpTool(value) {
|
|
57
|
-
return !!value && typeof value === "object" && value["__brand"] === "mcpTool" && typeof value.handler === "function";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
//#endregion
|
|
61
9
|
//#region src/server/principal.ts
|
|
62
10
|
const hasOwnData = (descriptors, key) => {
|
|
63
11
|
const descriptor = descriptors[key];
|
|
@@ -128,7 +76,7 @@ const DEFAULT_MCP_CONFIG = {
|
|
|
128
76
|
stdio: { trustedMaintenance: false },
|
|
129
77
|
execution: {}
|
|
130
78
|
};
|
|
131
|
-
function isPlainObject(value) {
|
|
79
|
+
function isPlainObject$1(value) {
|
|
132
80
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
133
81
|
}
|
|
134
82
|
const UNSAFE_CONFIG_KEYS = new Set([
|
|
@@ -137,7 +85,7 @@ const UNSAFE_CONFIG_KEYS = new Set([
|
|
|
137
85
|
"constructor"
|
|
138
86
|
]);
|
|
139
87
|
function ownDataEntries(value) {
|
|
140
|
-
if (!isPlainObject(value)) return [];
|
|
88
|
+
if (!isPlainObject$1(value)) return [];
|
|
141
89
|
try {
|
|
142
90
|
return Object.entries(Object.getOwnPropertyDescriptors(value)).filter(([key, descriptor]) => !UNSAFE_CONFIG_KEYS.has(key) && "value" in descriptor).map(([key, descriptor]) => [key, descriptor.value]);
|
|
143
91
|
} catch {
|
|
@@ -146,14 +94,14 @@ function ownDataEntries(value) {
|
|
|
146
94
|
}
|
|
147
95
|
function safeCloneConfigValue(value) {
|
|
148
96
|
if (Array.isArray(value)) return value.map((entry) => safeCloneConfigValue(entry));
|
|
149
|
-
if (!isPlainObject(value)) return value;
|
|
97
|
+
if (!isPlainObject$1(value)) return value;
|
|
150
98
|
const clone = Object.create(null);
|
|
151
99
|
for (const [key, entry] of ownDataEntries(value)) clone[key] = safeCloneConfigValue(entry);
|
|
152
100
|
return clone;
|
|
153
101
|
}
|
|
154
102
|
function mergeDeep(base, override) {
|
|
155
103
|
const out = safeCloneConfigValue(base);
|
|
156
|
-
for (const [key, value] of ownDataEntries(override)) if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeDeep(out[key], value);
|
|
104
|
+
for (const [key, value] of ownDataEntries(override)) if (isPlainObject$1(value) && isPlainObject$1(out[key])) out[key] = mergeDeep(out[key], value);
|
|
157
105
|
else if (value !== void 0) out[key] = safeCloneConfigValue(value);
|
|
158
106
|
return out;
|
|
159
107
|
}
|
|
@@ -363,6 +311,206 @@ async function evaluateMcpRule(rule, options) {
|
|
|
363
311
|
});
|
|
364
312
|
}
|
|
365
313
|
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/server/mcp-prompts.ts
|
|
316
|
+
/**
|
|
317
|
+
* Define an MCP prompt provider, contributed by a module the same way custom
|
|
318
|
+
* tools are (via `ModuleDefinition.mcpPrompts`, file convention
|
|
319
|
+
* `mcp-prompts/*.ts`). A provider answers `prompts/list` and `prompts/get`
|
|
320
|
+
* per request, so the prompts a caller sees can depend on who the caller is.
|
|
321
|
+
*
|
|
322
|
+
* `access` and `scopes` compose exactly like a custom tool's: a caller the
|
|
323
|
+
* provider denies sees none of its prompts, and `prompts/get` of any of them
|
|
324
|
+
* answers not-found. `list` and `get` then run with that caller's context and
|
|
325
|
+
* must apply their own row-level rules; returning `null` from `get` for a
|
|
326
|
+
* prompt the caller cannot read keeps it indistinguishable from an unknown
|
|
327
|
+
* name. Prompts are served on the `http` and `stdio` transports only, never to
|
|
328
|
+
* remote workloads.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```ts
|
|
332
|
+
* export default mcpPrompts("playbooks", {
|
|
333
|
+
* access: ({ session }) => !!session,
|
|
334
|
+
* scopes: "playbooks:read",
|
|
335
|
+
* list: async ({ ctx }) =>
|
|
336
|
+
* (await ctx.collections.playbooks.find({})).docs.map((doc) => ({
|
|
337
|
+
* name: doc.slug,
|
|
338
|
+
* description: doc.summary,
|
|
339
|
+
* })),
|
|
340
|
+
* get: async ({ ctx, name }) => {
|
|
341
|
+
* const doc = await ctx.collections.playbooks.findOne({ where: { slug: name } });
|
|
342
|
+
* if (!doc) return null;
|
|
343
|
+
* return { messages: [{ role: "user", content: { type: "text", text: doc.body } }] };
|
|
344
|
+
* },
|
|
345
|
+
* });
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
function mcpPrompts(name, config) {
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
__brand: "mcpPrompts",
|
|
351
|
+
name,
|
|
352
|
+
config: Object.freeze({
|
|
353
|
+
...config,
|
|
354
|
+
scopes: Array.isArray(config.scopes) ? [...config.scopes] : config.scopes
|
|
355
|
+
})
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function isMcpPromptProvider(value) {
|
|
359
|
+
if (!value || typeof value !== "object") return false;
|
|
360
|
+
const candidate = value;
|
|
361
|
+
return candidate.__brand === "mcpPrompts" && typeof candidate.config?.list === "function" && typeof candidate.config?.get === "function";
|
|
362
|
+
}
|
|
363
|
+
async function promptProviderAllows(scope, provider, ctx) {
|
|
364
|
+
return await evaluateMcpRule(provider.config.access, {
|
|
365
|
+
transport: scope.transport,
|
|
366
|
+
accessMode: scope.accessMode,
|
|
367
|
+
ctx
|
|
368
|
+
}) && scopeGateAllows(scopesFromContext(ctx), normalizeRequiredScopes(provider.config.scopes));
|
|
369
|
+
}
|
|
370
|
+
const MAX_INVALID_PARAMS_MESSAGE_CHARS = 512;
|
|
371
|
+
/**
|
|
372
|
+
* A provider's own `McpError(ErrorCode.InvalidParams, …)` is the one error
|
|
373
|
+
* that reaches the client as itself: it states what was wrong with the
|
|
374
|
+
* caller's arguments, which the provider authored for the caller. Every other
|
|
375
|
+
* throw stays the boundary's opaque `internal`.
|
|
376
|
+
*/
|
|
377
|
+
function providerInvalidParams(error) {
|
|
378
|
+
if (!(error instanceof McpError) || error.code !== ErrorCode.InvalidParams) return;
|
|
379
|
+
const message = error.message.replace(/^MCP error -?\d+: /, "").slice(0, MAX_INVALID_PARAMS_MESSAGE_CHARS);
|
|
380
|
+
return new McpError(ErrorCode.InvalidParams, message);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Install `prompts/list` and `prompts/get` over the released providers. The
|
|
384
|
+
* caller must declare the `prompts` capability on the server beforehand.
|
|
385
|
+
*/
|
|
386
|
+
function registerPromptProviders(server, scope, catalog) {
|
|
387
|
+
if (scope.workload || catalog.promptProviders.size === 0) return;
|
|
388
|
+
const providers = [...catalog.promptProviders.values()].map((entry) => entry.provider);
|
|
389
|
+
server.server.setRequestHandler(ListPromptsRequestSchema, (request, extra) => {
|
|
390
|
+
if (request.params?.cursor) throw new McpError(ErrorCode.InvalidParams, "Invalid cursor");
|
|
391
|
+
return scope.execution.execute({
|
|
392
|
+
operation: "prompts/list",
|
|
393
|
+
transport: scope.transport,
|
|
394
|
+
accessMode: scope.accessMode,
|
|
395
|
+
input: request.params ?? {},
|
|
396
|
+
extra,
|
|
397
|
+
authorize: () => scope.getContext(),
|
|
398
|
+
invoke: async (control) => {
|
|
399
|
+
const handlerArgs = {
|
|
400
|
+
...control,
|
|
401
|
+
transport: scope.transport,
|
|
402
|
+
accessMode: scope.accessMode,
|
|
403
|
+
request: scope.request
|
|
404
|
+
};
|
|
405
|
+
const prompts = [];
|
|
406
|
+
const names = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const provider of providers) {
|
|
408
|
+
if (!await promptProviderAllows(scope, provider, control.ctx)) continue;
|
|
409
|
+
for (const candidate of await provider.config.list(handlerArgs)) {
|
|
410
|
+
const parsed = PromptSchema.safeParse(candidate);
|
|
411
|
+
if (!parsed.success) throw new Error("MCP prompt provider listed an invalid prompt");
|
|
412
|
+
if (names.has(parsed.data.name)) continue;
|
|
413
|
+
names.add(parsed.data.name);
|
|
414
|
+
prompts.push(parsed.data);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { prompts };
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
server.server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => {
|
|
422
|
+
let invalidParams;
|
|
423
|
+
const rendered = await scope.execution.execute({
|
|
424
|
+
operation: "prompts/get",
|
|
425
|
+
transport: scope.transport,
|
|
426
|
+
accessMode: scope.accessMode,
|
|
427
|
+
input: request.params,
|
|
428
|
+
extra,
|
|
429
|
+
authorize: () => scope.getContext(),
|
|
430
|
+
invoke: async (control) => {
|
|
431
|
+
for (const provider of providers) {
|
|
432
|
+
if (!await promptProviderAllows(scope, provider, control.ctx)) continue;
|
|
433
|
+
let result;
|
|
434
|
+
try {
|
|
435
|
+
result = await provider.config.get({
|
|
436
|
+
...control,
|
|
437
|
+
transport: scope.transport,
|
|
438
|
+
accessMode: scope.accessMode,
|
|
439
|
+
request: scope.request,
|
|
440
|
+
name: request.params.name,
|
|
441
|
+
arguments: { ...request.params.arguments }
|
|
442
|
+
});
|
|
443
|
+
} catch (error) {
|
|
444
|
+
invalidParams = providerInvalidParams(error);
|
|
445
|
+
if (!invalidParams) throw error;
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
if (result === null || result === void 0) continue;
|
|
449
|
+
const parsed = GetPromptResultSchema.safeParse(result);
|
|
450
|
+
if (!parsed.success) throw new Error("MCP prompt provider rendered an invalid prompt");
|
|
451
|
+
return parsed.data;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (invalidParams) throw invalidParams;
|
|
457
|
+
if (!rendered) throw new McpError(ErrorCode.InvalidParams, "Prompt not found");
|
|
458
|
+
return rendered;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/server/mcp-tool.ts
|
|
464
|
+
var McpToolBuilder = class {
|
|
465
|
+
constructor(name, config) {
|
|
466
|
+
this.name = name;
|
|
467
|
+
this.config = config;
|
|
468
|
+
}
|
|
469
|
+
handler(handler) {
|
|
470
|
+
return Object.freeze({
|
|
471
|
+
__brand: "mcpTool",
|
|
472
|
+
name: this.name,
|
|
473
|
+
config: this.config,
|
|
474
|
+
handler
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* Define a custom MCP tool, contributed by a module the same way collections and
|
|
480
|
+
* routes are (via `ModuleDefinition.mcpTools`). `mcpTool(name, config).handler(fn)`
|
|
481
|
+
* returns a frozen, branded {@link McpToolDefinition}; the CRUD/route tools the
|
|
482
|
+
* framework auto-generates are the same shape.
|
|
483
|
+
*
|
|
484
|
+
* `config` mirrors the MCP tool contract: `inputSchema`/`outputSchema` (Zod),
|
|
485
|
+
* `title`/`description`/`annotations` for discovery, plus the two QUESTPIE
|
|
486
|
+
* authorization hooks that compose exactly like the built-in tools:
|
|
487
|
+
* - `access` — required explicit opt-in and an {@link McpAccessRule} run against the caller's transport /
|
|
488
|
+
* accessMode / session (and, for an OAuth caller, its scopes). Same RBAC-style
|
|
489
|
+
* gate the CRUD tools use.
|
|
490
|
+
* - `scopes` — the OAuth scopes an `oauth` caller must hold ({@link McpToolConfig.scopes}).
|
|
491
|
+
* Custom tools have no default scope mapping (unlike CRUD, there is no
|
|
492
|
+
* resource/operation to derive one from), so it is required; use `false` for
|
|
493
|
+
* an explicit no-OAuth-scope policy. The shipped scope gate ({@link scopeGateAllows}) enforces this at both
|
|
494
|
+
* `tools/list` (hidden) and `tools/call` (denied); `system`/`user` callers
|
|
495
|
+
* carry no scopes and skip it, so the effective bound is always `scopes ∩ RBAC`.
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* ```ts
|
|
499
|
+
* export const ping = mcpTool("ops.ping", {
|
|
500
|
+
* access: ({ session }) => !!session,
|
|
501
|
+
* description: "Health probe",
|
|
502
|
+
* inputSchema: z.object({}),
|
|
503
|
+
* scopes: "routes:ops/ping:invoke", // only OAuth callers holding this scope
|
|
504
|
+
* }).handler(async () => ({ content: [{ type: "text", text: "pong" }] }));
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
function mcpTool(name, config) {
|
|
508
|
+
return new McpToolBuilder(name, config);
|
|
509
|
+
}
|
|
510
|
+
function isMcpTool(value) {
|
|
511
|
+
return !!value && typeof value === "object" && value["__brand"] === "mcpTool" && typeof value.handler === "function";
|
|
512
|
+
}
|
|
513
|
+
|
|
366
514
|
//#endregion
|
|
367
515
|
//#region src/server/catalog.ts
|
|
368
516
|
const COLLECTION_OPERATION_KINDS = {
|
|
@@ -547,6 +695,15 @@ function resolveMcpCatalog(app, config) {
|
|
|
547
695
|
customTools.set(name, Object.freeze({ tool }));
|
|
548
696
|
addRequiredScopes(oauthScopes, tool.config.scopes);
|
|
549
697
|
}
|
|
698
|
+
const promptProviders = /* @__PURE__ */ new Map();
|
|
699
|
+
const configuredPromptProviders = app.state?.mcpPrompts ?? {};
|
|
700
|
+
for (const [key, candidate] of Object.entries(configuredPromptProviders)) {
|
|
701
|
+
if (!isMcpPromptProvider(candidate) || candidate.config.access === void 0 || candidate.config.access === false || candidate.config.scopes === void 0) continue;
|
|
702
|
+
const name = candidate.name || key;
|
|
703
|
+
if (promptProviders.has(name)) throw new Error(`Duplicate MCP prompt provider name: ${name}`);
|
|
704
|
+
promptProviders.set(name, Object.freeze({ provider: candidate }));
|
|
705
|
+
addRequiredScopes(oauthScopes, candidate.config.scopes);
|
|
706
|
+
}
|
|
550
707
|
const resourceConfig = ownSection(config, "resources");
|
|
551
708
|
const configuredResourceCollections = ownSection(resourceConfig, "collections");
|
|
552
709
|
const configuredResourceGlobals = ownSection(resourceConfig, "globals");
|
|
@@ -563,6 +720,7 @@ function resolveMcpCatalog(app, config) {
|
|
|
563
720
|
globals: readonlyMap(globals),
|
|
564
721
|
routes: readonlyMap(routes),
|
|
565
722
|
customTools: readonlyMap(customTools),
|
|
723
|
+
promptProviders: readonlyMap(promptProviders),
|
|
566
724
|
resources: Object.freeze({
|
|
567
725
|
collections: readonlySet(resourceCollections),
|
|
568
726
|
globals: readonlySet(resourceGlobals),
|
|
@@ -1073,11 +1231,123 @@ function toJsonSchema(schema) {
|
|
|
1073
1231
|
const compatible = jsonSchemaCompatibleSchema(schema);
|
|
1074
1232
|
if (!compatible) return void 0;
|
|
1075
1233
|
try {
|
|
1076
|
-
return z.toJSONSchema(compatible, { reused: "ref" });
|
|
1234
|
+
return applyMcpSchemaDiet(z.toJSONSchema(compatible, { reused: "ref" }));
|
|
1077
1235
|
} catch {
|
|
1078
1236
|
return;
|
|
1079
1237
|
}
|
|
1080
1238
|
}
|
|
1239
|
+
function isPlainObject(value) {
|
|
1240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1241
|
+
}
|
|
1242
|
+
const SCHEMA_MAP_KEYS = [
|
|
1243
|
+
"properties",
|
|
1244
|
+
"patternProperties",
|
|
1245
|
+
"dependentSchemas",
|
|
1246
|
+
"$defs",
|
|
1247
|
+
"definitions"
|
|
1248
|
+
];
|
|
1249
|
+
const NEUTRAL_SCHEMA_LIST_KEYS = [
|
|
1250
|
+
"anyOf",
|
|
1251
|
+
"allOf",
|
|
1252
|
+
"prefixItems"
|
|
1253
|
+
];
|
|
1254
|
+
const NEUTRAL_SCHEMA_KEYS = [
|
|
1255
|
+
"items",
|
|
1256
|
+
"then",
|
|
1257
|
+
"else",
|
|
1258
|
+
"propertyNames",
|
|
1259
|
+
"contains"
|
|
1260
|
+
];
|
|
1261
|
+
const NEUTRAL_MAYBE_BOOLEAN_SCHEMA_KEYS = [
|
|
1262
|
+
"additionalProperties",
|
|
1263
|
+
"unevaluatedProperties",
|
|
1264
|
+
"unevaluatedItems",
|
|
1265
|
+
"additionalItems"
|
|
1266
|
+
];
|
|
1267
|
+
/**
|
|
1268
|
+
* Rewrites a draft-07-style tuple (`items: [...]` + `additionalItems`) into
|
|
1269
|
+
* its 2020-12 equivalent (`prefixItems: [...]` + trailing `items`) in place.
|
|
1270
|
+
*
|
|
1271
|
+
* The MCP SDK's own `tools/list` projection (`zod-json-schema-compat.js`,
|
|
1272
|
+
* called with no `target`) emits JSON Schema **draft-07**, not 2020-12 — the
|
|
1273
|
+
* only place that difference is load-bearing for this codebase's schemas is
|
|
1274
|
+
* tuple typing, where draft-07's array-form `items` means something entirely
|
|
1275
|
+
* different in 2020-12 (a single "all items must match" schema). Our own
|
|
1276
|
+
* `toJsonSchema()` below (full zod's `z.toJSONSchema`) already emits native
|
|
1277
|
+
* 2020-12 `prefixItems`, so this is a no-op there. Converting first — for
|
|
1278
|
+
* every tuple anywhere in the tree, not just the root — makes the whole
|
|
1279
|
+
* document genuinely dialect-correct 2020-12 before `applyMcpSchemaDiet`
|
|
1280
|
+
* drops the top-level `$schema` pointer.
|
|
1281
|
+
*/
|
|
1282
|
+
function normalizeTupleDialect(node) {
|
|
1283
|
+
if (!Array.isArray(node.items) || node.prefixItems !== void 0) return;
|
|
1284
|
+
const tuple = node.items;
|
|
1285
|
+
const additionalItems = node.additionalItems;
|
|
1286
|
+
delete node.items;
|
|
1287
|
+
delete node.additionalItems;
|
|
1288
|
+
node.prefixItems = tuple;
|
|
1289
|
+
if (additionalItems === false) node.items = false;
|
|
1290
|
+
else if (isPlainObject(additionalItems)) node.items = additionalItems;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Strips wire noise the zod v4 / MCP SDK JSON Schema codecs emit on every
|
|
1294
|
+
* tool schema, without touching example/default/const/enum payloads (those
|
|
1295
|
+
* carry caller data, not schema structure, and must never be walked).
|
|
1296
|
+
*
|
|
1297
|
+
* - Removes the document-level `$schema` dialect pointer (informational only;
|
|
1298
|
+
* `normalizeTupleDialect` above makes the document genuinely 2020-12-shaped
|
|
1299
|
+
* first, so an absent `$schema` is universally, correctly, read as 2020-12).
|
|
1300
|
+
* - Drops `additionalProperties: false`, *except inside a `oneOf`/`not`/`if`
|
|
1301
|
+
* subtree*. Ordinarily this is safe to drop everywhere: the MCP SDK
|
|
1302
|
+
* validates tool arguments against the *original* zod schema at call time
|
|
1303
|
+
* (`McpServer#validateToolInput` in `mcp.js`), never against this projected
|
|
1304
|
+
* JSON Schema, so removing the annotation does not change what a call gets
|
|
1305
|
+
* rejected for. But inside `oneOf`, `additionalProperties: false` can be
|
|
1306
|
+
* the only thing making two branches mutually exclusive (reproduced:
|
|
1307
|
+
* `z.xor` of two `.strict()` objects with disjoint keys — stripping it from
|
|
1308
|
+
* both branches makes a value that should match exactly one branch match
|
|
1309
|
+
* both, which every `oneOf`-aware validator then rejects as ambiguous, even
|
|
1310
|
+
* though the MCP server itself would still accept it). Inside `not`, it can
|
|
1311
|
+
* be the very thing being negated — stripping it *widens* what `not`
|
|
1312
|
+
* excludes, which can turn a previously-schema-valid value into
|
|
1313
|
+
* schema-invalid for any client-side validator. `if` shares the same
|
|
1314
|
+
* "widening changes routing" hazard. `anyOf`/`allOf` don't have this
|
|
1315
|
+
* problem (a widened disjunct can never invalidate an already-valid input),
|
|
1316
|
+
* so only `oneOf`/`not`/`if` propagate the protection flag.
|
|
1317
|
+
* - Drops the `pattern` on a `format: "uuid"` node, keeping `format`. The
|
|
1318
|
+
* pattern is the zod-generated uuid regex that always accompanies
|
|
1319
|
+
* `format: "uuid"` in this codebase's schemas (there is no path that sets
|
|
1320
|
+
* `format: "uuid"` other than zod's own `.uuid()`), and — same reasoning as
|
|
1321
|
+
* `additionalProperties` — the MCP SDK never validates against this
|
|
1322
|
+
* projected schema, so the regex is redundant here. `pattern` on any other
|
|
1323
|
+
* node — a node without `format: "uuid"` — is a user-authored constraint
|
|
1324
|
+
* and is left untouched.
|
|
1325
|
+
*/
|
|
1326
|
+
function walkSchemaNode(node, protectAdditionalProperties = false) {
|
|
1327
|
+
if (!isPlainObject(node)) return;
|
|
1328
|
+
normalizeTupleDialect(node);
|
|
1329
|
+
if (node.format === "uuid" && typeof node.pattern === "string") delete node.pattern;
|
|
1330
|
+
if (!protectAdditionalProperties && node.additionalProperties === false) delete node.additionalProperties;
|
|
1331
|
+
for (const key of SCHEMA_MAP_KEYS) {
|
|
1332
|
+
const map = node[key];
|
|
1333
|
+
if (isPlainObject(map)) for (const value of Object.values(map)) walkSchemaNode(value, protectAdditionalProperties);
|
|
1334
|
+
}
|
|
1335
|
+
for (const key of NEUTRAL_SCHEMA_LIST_KEYS) {
|
|
1336
|
+
const list = node[key];
|
|
1337
|
+
if (Array.isArray(list)) for (const item of list) walkSchemaNode(item, protectAdditionalProperties);
|
|
1338
|
+
}
|
|
1339
|
+
for (const key of NEUTRAL_SCHEMA_KEYS) walkSchemaNode(node[key], protectAdditionalProperties);
|
|
1340
|
+
for (const key of NEUTRAL_MAYBE_BOOLEAN_SCHEMA_KEYS) walkSchemaNode(node[key], protectAdditionalProperties);
|
|
1341
|
+
if (Array.isArray(node.oneOf)) for (const item of node.oneOf) walkSchemaNode(item, true);
|
|
1342
|
+
walkSchemaNode(node.not, true);
|
|
1343
|
+
walkSchemaNode(node.if, true);
|
|
1344
|
+
}
|
|
1345
|
+
function applyMcpSchemaDiet(schema) {
|
|
1346
|
+
if (!isPlainObject(schema)) return schema;
|
|
1347
|
+
delete schema.$schema;
|
|
1348
|
+
walkSchemaNode(schema);
|
|
1349
|
+
return schema;
|
|
1350
|
+
}
|
|
1081
1351
|
function toToolInputJsonSchema(schema) {
|
|
1082
1352
|
const parsed = ToolSchema.shape.inputSchema.safeParse(toJsonSchema(schema));
|
|
1083
1353
|
return parsed.success ? parsed.data : {
|
|
@@ -2385,6 +2655,46 @@ async function registerRouteTools(server, scope, catalog) {
|
|
|
2385
2655
|
|
|
2386
2656
|
//#endregion
|
|
2387
2657
|
//#region src/server/create-server.ts
|
|
2658
|
+
function isProductionEnvironment() {
|
|
2659
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Reports a schema-diet install failure loudly instead of either crashing
|
|
2663
|
+
* every session (a renamed/removed SDK internal would otherwise throw deep
|
|
2664
|
+
* inside request handling on the very first `tools/list` call) or failing
|
|
2665
|
+
* silently (shipping an undieted catalogue with nobody told why). Always
|
|
2666
|
+
* logs; only throws outside production, so a CI/dev run surfaces the break
|
|
2667
|
+
* immediately while a live server degrades to the SDK's own undieted
|
|
2668
|
+
* `tools/list` response.
|
|
2669
|
+
*/
|
|
2670
|
+
function reportSchemaDietInstallFailure(message) {
|
|
2671
|
+
const prefixed = `@questpie/mcp: ${message}`;
|
|
2672
|
+
console.error(prefixed);
|
|
2673
|
+
if (!isProductionEnvironment()) throw new Error(prefixed);
|
|
2674
|
+
}
|
|
2675
|
+
function installSchemaDietListToolsHandler(server) {
|
|
2676
|
+
const handlers = Reflect.get(server.server, "_requestHandlers");
|
|
2677
|
+
if (!(handlers instanceof Map)) {
|
|
2678
|
+
reportSchemaDietInstallFailure("could not install the tools/list schema diet — McpServer#server's internal request-handler map is missing or not a Map (the MCP SDK's internals likely changed shape). Tools will be listed without the diet applied.");
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
const defaultHandler = handlers.get("tools/list");
|
|
2682
|
+
if (!defaultHandler) {
|
|
2683
|
+
reportSchemaDietInstallFailure("could not install the tools/list schema diet — no default tools/list handler was registered even though at least one tool exists. Tools will be listed without the diet applied.");
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
|
|
2687
|
+
const result = await defaultHandler(request, extra);
|
|
2688
|
+
return {
|
|
2689
|
+
...result,
|
|
2690
|
+
tools: result.tools.map((tool) => ({
|
|
2691
|
+
...tool,
|
|
2692
|
+
inputSchema: applyMcpSchemaDiet(tool.inputSchema),
|
|
2693
|
+
...tool.outputSchema ? { outputSchema: applyMcpSchemaDiet(tool.outputSchema) } : {}
|
|
2694
|
+
}))
|
|
2695
|
+
};
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2388
2698
|
async function createServer(app, options, workload) {
|
|
2389
2699
|
const release = workload ? createWorkloadMcpRelease(app, options.config) : options.config ? createIsolatedMcpRelease(app, options.config) : getAppMcpRelease(app);
|
|
2390
2700
|
const { config, catalog } = release;
|
|
@@ -2401,14 +2711,17 @@ async function createServer(app, options, workload) {
|
|
|
2401
2711
|
version: config.version ?? "0.0.0"
|
|
2402
2712
|
}, { capabilities: {
|
|
2403
2713
|
tools: {},
|
|
2404
|
-
...catalog.resources.collections.size > 0 || catalog.resources.globals.size > 0 || catalog.resources.routes.size > 0 ? { resources: {} } : {}
|
|
2714
|
+
...catalog.resources.collections.size > 0 || catalog.resources.globals.size > 0 || catalog.resources.routes.size > 0 ? { resources: {} } : {},
|
|
2715
|
+
...!workload && catalog.promptProviders.size > 0 ? { prompts: {} } : {}
|
|
2405
2716
|
} });
|
|
2406
2717
|
await registerCrudTools(server, scope, config, catalog);
|
|
2407
2718
|
await registerRouteTools(server, scope, catalog);
|
|
2408
2719
|
registerSchemaResources(server, scope, catalog);
|
|
2409
2720
|
await registerCustomTools(server, scope, catalog);
|
|
2721
|
+
registerPromptProviders(server, scope, catalog);
|
|
2410
2722
|
if (workload) server.server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => listWorkloadTools(workload, release.execution, extra));
|
|
2411
2723
|
else if ([...catalog.collections.values()].every((entry) => entry.operations.length === 0) && [...catalog.globals.values()].every((entry) => entry.operations.length === 0) && catalog.routes.size === 0 && catalog.customTools.size === 0) server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
|
|
2724
|
+
else installSchemaDietListToolsHandler(server);
|
|
2412
2725
|
return server;
|
|
2413
2726
|
}
|
|
2414
2727
|
function resolveStdioAccessMode(options, trustedMaintenance) {
|
|
@@ -2432,4 +2745,4 @@ async function createWorkloadMcpServer(app, options) {
|
|
|
2432
2745
|
}
|
|
2433
2746
|
|
|
2434
2747
|
//#endregion
|
|
2435
|
-
export {
|
|
2748
|
+
export { scopeGateAllows as C, requiredScopesForOperation as S, validateMcpPrincipal as T, resolveMcpCatalog as _, customToolWorkloadIdentity as a, defaultOperationScope as b, createWorkloadMcpBoundary as c, getAppMcpRelease as d, McpAccessDeniedError as f, toMcpToolError as g, mcpPublicErrorCode as h, customToolDescriptor as i, createRuntimeScope as l, assertBoundedMcpValue as m, createWorkloadMcpServer as n, executeCustomToolCall as o, McpPublicError as p, customToolAllows as r, authorizeWorkload as s, createMcpServer as t, createWorkloadMcpRelease as u, mcpTool as v, scopesFromContext as w, normalizeRequiredScopes as x, mcpPrompts as y };
|
|
@@ -37,6 +37,7 @@ type McpRoutes = {
|
|
|
37
37
|
};
|
|
38
38
|
type McpFieldTypes = Record<never, never>;
|
|
39
39
|
type McpMcpTools = Record<never, never>;
|
|
40
|
+
type McpMcpPrompts = Record<never, never>;
|
|
40
41
|
//#endregion
|
|
41
42
|
//#region src/server/modules/mcp/index.d.ts
|
|
42
43
|
type McpModule = typeof mcpModule;
|
|
@@ -52,6 +53,7 @@ declare const mcpModule: {
|
|
|
52
53
|
readonly jobs: McpJobs;
|
|
53
54
|
readonly fieldTypes: McpFieldTypes;
|
|
54
55
|
readonly mcpTools: McpMcpTools;
|
|
56
|
+
readonly mcpPrompts: McpMcpPrompts;
|
|
55
57
|
readonly messages: Record<never, never>;
|
|
56
58
|
readonly services: Record<never, never>;
|
|
57
59
|
readonly emailTemplates: Record<never, never>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as mcpModule } from "./index-
|
|
2
|
-
import { A as
|
|
1
|
+
import { n as mcpModule } from "./index-CLEEqMqK.mjs";
|
|
2
|
+
import { A as McpTransportKind, B as McpWorkloadRequirement, C as McpRequiredScopes, D as McpToolConfig, E as McpStdioConfig, F as McpWorkloadContextBinder, H as McpWorkloadToolKind, I as McpWorkloadContextBindingInput, L as McpWorkloadExecutionControl, M as McpWorkloadAuthorization, N as McpWorkloadAuthorizationRequest, O as McpToolDefinition, P as McpWorkloadAuthorizer, R as McpWorkloadHandoff, S as McpPublicErrorCode, T as McpRoutesConfig, U as McpWorkloadToolPort, V as McpWorkloadToolFacts, W as WorkloadMcpServerOptions, _ as McpProgrammaticToolResult, a as McpAccessRule, b as McpPromptProviderConfig, c as McpCrudConfig, d as McpExecutionDiagnosticEvent, f as McpExecutionLimits, g as McpProgrammaticTool, h as McpProgrammaticRequestOptions, i as McpAccessMode, j as McpWorkloadAuditEvent, k as McpToolHandlerArgs, l as McpEntityPolicy, m as McpHttpConfig, n as QuestpieApp, o as McpAccessRuleContext, p as McpExecutionOptions, r as mcpPublicErrorCode, s as McpConfig, t as startStdioServer, u as McpExecutionConfig, v as McpPromptGetArgs, w as McpResourcesConfig, x as McpPromptProviderDefinition, y as McpPromptHandlerArgs, z as McpWorkloadHandoffInput } from "./stdio-C2V13-vP.mjs";
|
|
3
3
|
import { t as mcpPlugin } from "./plugin-CMR-wkeP.mjs";
|
|
4
4
|
import { AppContext, Questpie, RequestContext, ScopeOperationKind, defaultOperationScope, introspectRoutes } from "questpie";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -100,11 +100,15 @@ interface ResolvedMcpRouteCatalogEntry extends ResolvedMcpEntityCatalogEntry {
|
|
|
100
100
|
interface ResolvedMcpCustomToolCatalogEntry {
|
|
101
101
|
tool: McpToolDefinition;
|
|
102
102
|
}
|
|
103
|
+
interface ResolvedMcpPromptProviderCatalogEntry {
|
|
104
|
+
provider: McpPromptProviderDefinition;
|
|
105
|
+
}
|
|
103
106
|
interface ResolvedMcpCatalog {
|
|
104
107
|
collections: ReadonlyMap<string, ResolvedMcpEntityCatalogEntry>;
|
|
105
108
|
globals: ReadonlyMap<string, ResolvedMcpEntityCatalogEntry>;
|
|
106
109
|
routes: ReadonlyMap<string, ResolvedMcpRouteCatalogEntry>;
|
|
107
110
|
customTools: ReadonlyMap<string, ResolvedMcpCustomToolCatalogEntry>;
|
|
111
|
+
promptProviders: ReadonlyMap<string, ResolvedMcpPromptProviderCatalogEntry>;
|
|
108
112
|
resources: {
|
|
109
113
|
collections: ReadonlySet<string>;
|
|
110
114
|
globals: ReadonlySet<string>;
|
|
@@ -118,6 +122,7 @@ interface ResolvedMcpCatalog {
|
|
|
118
122
|
type CatalogApp = Pick<Questpie<any>, "getCollections" | "getGlobals"> & {
|
|
119
123
|
state?: {
|
|
120
124
|
mcpTools?: Record<string, unknown>;
|
|
125
|
+
mcpPrompts?: Record<string, unknown>;
|
|
121
126
|
} & Record<string, unknown>;
|
|
122
127
|
config?: Questpie<any>["config"];
|
|
123
128
|
};
|
|
@@ -164,4 +169,39 @@ declare class McpToolBuilder<TInputSchema extends z.ZodTypeAny, TOutputSchema ex
|
|
|
164
169
|
*/
|
|
165
170
|
declare function mcpTool<TInputSchema extends z.ZodTypeAny = z.ZodTypeAny, TOutputSchema extends z.ZodTypeAny = z.ZodTypeAny>(name: string, config: McpToolConfig<TInputSchema, TOutputSchema>): McpToolBuilder<TInputSchema, TOutputSchema>;
|
|
166
171
|
//#endregion
|
|
167
|
-
|
|
172
|
+
//#region src/server/mcp-prompts.d.ts
|
|
173
|
+
/**
|
|
174
|
+
* Define an MCP prompt provider, contributed by a module the same way custom
|
|
175
|
+
* tools are (via `ModuleDefinition.mcpPrompts`, file convention
|
|
176
|
+
* `mcp-prompts/*.ts`). A provider answers `prompts/list` and `prompts/get`
|
|
177
|
+
* per request, so the prompts a caller sees can depend on who the caller is.
|
|
178
|
+
*
|
|
179
|
+
* `access` and `scopes` compose exactly like a custom tool's: a caller the
|
|
180
|
+
* provider denies sees none of its prompts, and `prompts/get` of any of them
|
|
181
|
+
* answers not-found. `list` and `get` then run with that caller's context and
|
|
182
|
+
* must apply their own row-level rules; returning `null` from `get` for a
|
|
183
|
+
* prompt the caller cannot read keeps it indistinguishable from an unknown
|
|
184
|
+
* name. Prompts are served on the `http` and `stdio` transports only, never to
|
|
185
|
+
* remote workloads.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* export default mcpPrompts("playbooks", {
|
|
190
|
+
* access: ({ session }) => !!session,
|
|
191
|
+
* scopes: "playbooks:read",
|
|
192
|
+
* list: async ({ ctx }) =>
|
|
193
|
+
* (await ctx.collections.playbooks.find({})).docs.map((doc) => ({
|
|
194
|
+
* name: doc.slug,
|
|
195
|
+
* description: doc.summary,
|
|
196
|
+
* })),
|
|
197
|
+
* get: async ({ ctx, name }) => {
|
|
198
|
+
* const doc = await ctx.collections.playbooks.findOne({ where: { slug: name } });
|
|
199
|
+
* if (!doc) return null;
|
|
200
|
+
* return { messages: [{ role: "user", content: { type: "text", text: doc.body } }] };
|
|
201
|
+
* },
|
|
202
|
+
* });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
declare function mcpPrompts(name: string, config: McpPromptProviderConfig): McpPromptProviderDefinition;
|
|
206
|
+
//#endregion
|
|
207
|
+
export { type McpAccessMode, type McpAccessRule, type McpAccessRuleContext, type McpConfig, type McpCrudConfig, type McpEntityPolicy, type McpExecutionConfig, type McpExecutionDiagnosticEvent, type McpExecutionLimits, type McpExecutionOptions, type McpHttpConfig, type McpProgrammaticRequestOptions, type McpProgrammaticTool, type McpProgrammaticToolResult, type McpPromptGetArgs, type McpPromptHandlerArgs, type McpPromptProviderConfig, type McpPromptProviderDefinition, type McpPublicErrorCode, type McpRequiredScopes, type McpResourcesConfig, type McpRoutesConfig, type McpStdioConfig, type McpToolConfig, type McpToolDefinition, type McpToolHandlerArgs, type McpTransportKind, type McpWorkloadAuditEvent, type McpWorkloadAuthorization, type McpWorkloadAuthorizationRequest, type McpWorkloadAuthorizer, type McpWorkloadContextBinder, type McpWorkloadContextBindingInput, type McpWorkloadExecutionControl, type McpWorkloadHandoff, type McpWorkloadHandoffInput, type McpWorkloadRequirement, type McpWorkloadToolFacts, type McpWorkloadToolKind, type McpWorkloadToolPort, type ResolvedMcpCatalog, type ResolvedMcpCustomToolCatalogEntry, type ResolvedMcpEntityCatalogEntry, type ResolvedMcpPromptProviderCatalogEntry, type ResolvedMcpRouteCatalogEntry, type ScopeOperationKind, type WorkloadMcpServerOptions, createMcpServer, createWorkloadMcpServer, createWorkloadMcpToolPort, mcpModule as default, mcpModule, defaultOperationScope, mcpConfig, mcpPlugin, mcpPrompts, mcpPublicErrorCode, mcpTool, normalizeRequiredScopes, requiredScopesForOperation, resolveMcpCatalog, scopeGateAllows, scopesFromContext, startStdioServer };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as
|
|
2
|
-
import { n as mcp_default, t as mcpModule } from "./mcp-
|
|
3
|
-
import { t as mcpPlugin } from "./plugin-
|
|
4
|
-
import { t as startStdioServer } from "./stdio-
|
|
1
|
+
import { C as scopeGateAllows, S as requiredScopesForOperation, _ as resolveMcpCatalog, a as customToolWorkloadIdentity, b as defaultOperationScope, c as createWorkloadMcpBoundary, f as McpAccessDeniedError, g as toMcpToolError, h as mcpPublicErrorCode, i as customToolDescriptor, l as createRuntimeScope, m as assertBoundedMcpValue, n as createWorkloadMcpServer, o as executeCustomToolCall, p as McpPublicError, r as customToolAllows, s as authorizeWorkload, t as createMcpServer, u as createWorkloadMcpRelease, v as mcpTool, w as scopesFromContext, x as normalizeRequiredScopes, y as mcpPrompts } from "./create-server-DzTo-3Wy.mjs";
|
|
2
|
+
import { n as mcp_default, t as mcpModule } from "./mcp-C55EQ-yd.mjs";
|
|
3
|
+
import { t as mcpPlugin } from "./plugin-BgwWI9S8.mjs";
|
|
4
|
+
import { t as startStdioServer } from "./stdio-Bq7nzR79.mjs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
|
|
7
7
|
//#region src/server/workload-tool-port.ts
|
|
@@ -96,4 +96,4 @@ function mcpConfig(config) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
//#endregion
|
|
99
|
-
export { createMcpServer, createWorkloadMcpServer, createWorkloadMcpToolPort, mcp_default as default, defaultOperationScope, mcpConfig, mcpModule, mcpPlugin, mcpPublicErrorCode, mcpTool, normalizeRequiredScopes, requiredScopesForOperation, resolveMcpCatalog, scopeGateAllows, scopesFromContext, startStdioServer };
|
|
99
|
+
export { createMcpServer, createWorkloadMcpServer, createWorkloadMcpToolPort, mcp_default as default, defaultOperationScope, mcpConfig, mcpModule, mcpPlugin, mcpPrompts, mcpPublicErrorCode, mcpTool, normalizeRequiredScopes, requiredScopesForOperation, resolveMcpCatalog, scopeGateAllows, scopesFromContext, startStdioServer };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as mcpPlugin } from "./plugin-
|
|
1
|
+
import { T as validateMcpPrincipal, d as getAppMcpRelease, m as assertBoundedMcpValue, t as createMcpServer } from "./create-server-DzTo-3Wy.mjs";
|
|
2
|
+
import { t as mcpPlugin } from "./plugin-BgwWI9S8.mjs";
|
|
3
3
|
import { route } from "questpie";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
5
|
|
|
@@ -325,6 +325,7 @@ const _module = {
|
|
|
325
325
|
seeds: [],
|
|
326
326
|
fieldTypes: {},
|
|
327
327
|
mcpTools: {},
|
|
328
|
+
mcpPrompts: {},
|
|
328
329
|
plugin: plugin_default
|
|
329
330
|
};
|
|
330
331
|
var module_default = _module;
|
package/dist/modules/mcp.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as mcpModule, t as McpModule } from "../index-
|
|
1
|
+
import { n as mcpModule, t as McpModule } from "../index-CLEEqMqK.mjs";
|
|
2
2
|
export { type McpModule, mcpModule };
|
package/dist/modules/mcp.mjs
CHANGED
|
@@ -5,15 +5,26 @@ function mcpPlugin() {
|
|
|
5
5
|
targets: { server: {
|
|
6
6
|
root: ".",
|
|
7
7
|
outputFile: "index.ts",
|
|
8
|
-
categories: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
categories: {
|
|
9
|
+
mcpTools: {
|
|
10
|
+
dirs: ["mcp-tools"],
|
|
11
|
+
prefix: "mcpTool",
|
|
12
|
+
factoryFunctions: ["mcpTool"],
|
|
13
|
+
registryKey: true,
|
|
14
|
+
includeInAppState: true,
|
|
15
|
+
extractFromModules: true,
|
|
16
|
+
typeEmit: "standard"
|
|
17
|
+
},
|
|
18
|
+
mcpPrompts: {
|
|
19
|
+
dirs: ["mcp-prompts"],
|
|
20
|
+
prefix: "mcpPrompts",
|
|
21
|
+
factoryFunctions: ["mcpPrompts"],
|
|
22
|
+
registryKey: true,
|
|
23
|
+
includeInAppState: true,
|
|
24
|
+
extractFromModules: true,
|
|
25
|
+
typeEmit: "standard"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
17
28
|
discover: { mcpConfig: {
|
|
18
29
|
pattern: "config/mcp.ts",
|
|
19
30
|
configKey: "mcp"
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AppContext, RequestContext, createContextFactory } from "questpie";
|
|
2
|
+
import { CallToolResult, GetPromptResult, Prompt, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import "questpie/executor";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import * as _modelcontextprotocol_sdk_server_mcp0 from "@modelcontextprotocol/sdk/server/mcp";
|
|
6
6
|
|
|
7
7
|
//#region src/server/types.d.ts
|
|
@@ -303,12 +303,58 @@ interface McpToolDefinition<TInputSchema extends z.ZodTypeAny = z.ZodTypeAny, TO
|
|
|
303
303
|
readonly config: McpToolConfig<TInputSchema, TOutputSchema>;
|
|
304
304
|
readonly handler: (args: McpToolHandlerArgs<z.infer<TInputSchema>>) => CallToolResult | Promise<CallToolResult>;
|
|
305
305
|
}
|
|
306
|
+
/** Per-request arguments shared by a prompt provider's `list` and `get`. */
|
|
307
|
+
interface McpPromptHandlerArgs {
|
|
308
|
+
ctx: AppContext & Partial<RequestContext>;
|
|
309
|
+
transport: McpTransportKind;
|
|
310
|
+
accessMode: McpAccessMode;
|
|
311
|
+
request?: Request;
|
|
312
|
+
signal: AbortSignal;
|
|
313
|
+
requestId: string | number;
|
|
314
|
+
correlationId: string;
|
|
315
|
+
}
|
|
316
|
+
interface McpPromptGetArgs extends McpPromptHandlerArgs {
|
|
317
|
+
/** The prompt name the client asked for, exactly as sent. */
|
|
318
|
+
name: string;
|
|
319
|
+
/** The client's string arguments; `{}` when the client sent none. */
|
|
320
|
+
arguments: Record<string, string>;
|
|
321
|
+
}
|
|
322
|
+
interface McpPromptProviderConfig {
|
|
323
|
+
/**
|
|
324
|
+
* Required explicit opt-in, evaluated per request against the same caller
|
|
325
|
+
* context the tools get. A denied caller sees none of this provider's prompts
|
|
326
|
+
* and a `prompts/get` of any of them answers not-found.
|
|
327
|
+
*/
|
|
328
|
+
access: McpAccessRule;
|
|
329
|
+
/**
|
|
330
|
+
* Scopes an OAuth caller must hold (all required — AND), or `false` for an
|
|
331
|
+
* explicit no-OAuth-scope policy. Required, like a custom tool's `scopes`.
|
|
332
|
+
*/
|
|
333
|
+
scopes: McpRequiredScopes;
|
|
334
|
+
/**
|
|
335
|
+
* The prompts this caller can use right now. Runs on every `prompts/list`;
|
|
336
|
+
* the list may differ per caller and per request.
|
|
337
|
+
*/
|
|
338
|
+
list: (args: McpPromptHandlerArgs) => Prompt[] | Promise<Prompt[]>;
|
|
339
|
+
/**
|
|
340
|
+
* Render one prompt, or return `null` when this caller has no prompt of
|
|
341
|
+
* that name. `null` is answered as not-found, the same as an unknown name,
|
|
342
|
+
* so a prompt the caller cannot read is indistinguishable from none.
|
|
343
|
+
*/
|
|
344
|
+
get: (args: McpPromptGetArgs) => GetPromptResult | null | Promise<GetPromptResult | null>;
|
|
345
|
+
}
|
|
346
|
+
interface McpPromptProviderDefinition {
|
|
347
|
+
readonly __brand: "mcpPrompts";
|
|
348
|
+
readonly name: string;
|
|
349
|
+
readonly config: McpPromptProviderConfig;
|
|
350
|
+
}
|
|
306
351
|
declare module "questpie" {
|
|
307
352
|
interface AppStateConfig {
|
|
308
353
|
mcp?: McpConfig;
|
|
309
354
|
}
|
|
310
355
|
interface ModuleDefinition {
|
|
311
356
|
mcpTools?: Record<string, McpToolDefinition>;
|
|
357
|
+
mcpPrompts?: Record<string, McpPromptProviderDefinition>;
|
|
312
358
|
}
|
|
313
359
|
}
|
|
314
360
|
//#endregion
|
|
@@ -322,4 +368,4 @@ type QuestpieApp = Parameters<typeof createContextFactory>[0];
|
|
|
322
368
|
//#region src/server/stdio.d.ts
|
|
323
369
|
declare function startStdioServer(app: QuestpieApp, options?: Omit<McpExecutionOptions, "transport">): Promise<_modelcontextprotocol_sdk_server_mcp0.McpServer>;
|
|
324
370
|
//#endregion
|
|
325
|
-
export {
|
|
371
|
+
export { McpTransportKind as A, McpWorkloadRequirement as B, McpRequiredScopes as C, McpToolConfig as D, McpStdioConfig as E, McpWorkloadContextBinder as F, McpWorkloadToolKind as H, McpWorkloadContextBindingInput as I, McpWorkloadExecutionControl as L, McpWorkloadAuthorization as M, McpWorkloadAuthorizationRequest as N, McpToolDefinition as O, McpWorkloadAuthorizer as P, McpWorkloadHandoff as R, McpPublicErrorCode as S, McpRoutesConfig as T, McpWorkloadToolPort as U, McpWorkloadToolFacts as V, WorkloadMcpServerOptions as W, McpProgrammaticToolResult as _, McpAccessRule as a, McpPromptProviderConfig as b, McpCrudConfig as c, McpExecutionDiagnosticEvent as d, McpExecutionLimits as f, McpProgrammaticTool as g, McpProgrammaticRequestOptions as h, McpAccessMode as i, McpWorkloadAuditEvent as j, McpToolHandlerArgs as k, McpEntityPolicy as l, McpHttpConfig as m, QuestpieApp as n, McpAccessRuleContext as o, McpExecutionOptions as p, mcpPublicErrorCode as r, McpConfig as s, startStdioServer as t, McpExecutionConfig as u, McpPromptGetArgs as v, McpResourcesConfig as w, McpPromptProviderDefinition as x, McpPromptHandlerArgs as y, McpWorkloadHandoffInput as z };
|
package/dist/stdio.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as startStdioServer } from "./stdio-
|
|
1
|
+
import { t as startStdioServer } from "./stdio-C2V13-vP.mjs";
|
|
2
2
|
export { startStdioServer };
|
package/dist/stdio.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@questpie/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.0",
|
|
4
4
|
"description": "Model Context Protocol server for QUESTPIE apps, under the app's own access rules",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
73
|
-
"questpie": "^3.
|
|
73
|
+
"questpie": "^3.29.0",
|
|
74
74
|
"zod": "^4.2.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
@@ -80,6 +80,6 @@
|
|
|
80
80
|
"tsdown": "^0.18.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"questpie": "^3.
|
|
83
|
+
"questpie": "^3.29.0"
|
|
84
84
|
}
|
|
85
85
|
}
|