@questpie/mcp 3.28.11 → 3.30.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 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 factory |
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];
@@ -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),
@@ -2553,12 +2711,14 @@ async function createServer(app, options, workload) {
2553
2711
  version: config.version ?? "0.0.0"
2554
2712
  }, { capabilities: {
2555
2713
  tools: {},
2556
- ...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: {} } : {}
2557
2716
  } });
2558
2717
  await registerCrudTools(server, scope, config, catalog);
2559
2718
  await registerRouteTools(server, scope, catalog);
2560
2719
  registerSchemaResources(server, scope, catalog);
2561
2720
  await registerCustomTools(server, scope, catalog);
2721
+ registerPromptProviders(server, scope, catalog);
2562
2722
  if (workload) server.server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => listWorkloadTools(workload, release.execution, extra));
2563
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: [] }));
2564
2724
  else installSchemaDietListToolsHandler(server);
@@ -2585,4 +2745,4 @@ async function createWorkloadMcpServer(app, options) {
2585
2745
  }
2586
2746
 
2587
2747
  //#endregion
2588
- export { validateMcpPrincipal as C, scopesFromContext as S, resolveMcpCatalog as _, customToolWorkloadIdentity as a, requiredScopesForOperation 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, defaultOperationScope as v, mcpTool as w, scopeGateAllows as x, normalizeRequiredScopes as y };
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-C3gm6ar5.mjs";
2
- import { A as McpWorkloadAuthorizer, B as WorkloadMcpServerOptions, C as McpToolConfig, D as McpWorkloadAuditEvent, E as McpTransportKind, F as McpWorkloadHandoffInput, I as McpWorkloadRequirement, L as McpWorkloadToolFacts, M as McpWorkloadContextBindingInput, N as McpWorkloadExecutionControl, O as McpWorkloadAuthorization, P as McpWorkloadHandoff, R as McpWorkloadToolKind, S as McpStdioConfig, T as McpToolHandlerArgs, _ as McpProgrammaticToolResult, a as McpAccessRule, b as McpResourcesConfig, c as McpCrudConfig, d as McpExecutionDiagnosticEvent, f as McpExecutionLimits, g as McpProgrammaticTool, h as McpProgrammaticRequestOptions, i as McpAccessMode, j as McpWorkloadContextBinder, k as McpWorkloadAuthorizationRequest, 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 McpPublicErrorCode, w as McpToolDefinition, x as McpRoutesConfig, y as McpRequiredScopes, z as McpWorkloadToolPort } from "./stdio-C3663EKG.mjs";
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
- 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 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 ResolvedMcpRouteCatalogEntry, type ScopeOperationKind, type WorkloadMcpServerOptions, createMcpServer, createWorkloadMcpServer, createWorkloadMcpToolPort, mcpModule as default, mcpModule, defaultOperationScope, mcpConfig, mcpPlugin, mcpPublicErrorCode, mcpTool, normalizeRequiredScopes, requiredScopesForOperation, resolveMcpCatalog, scopeGateAllows, scopesFromContext, startStdioServer };
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 scopesFromContext, _ as resolveMcpCatalog, a as customToolWorkloadIdentity, b as requiredScopesForOperation, 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 defaultOperationScope, w as mcpTool, x as scopeGateAllows, y as normalizeRequiredScopes } from "./create-server-5Eo4QP3q.mjs";
2
- import { n as mcp_default, t as mcpModule } from "./mcp-i0Kqu_bB.mjs";
3
- import { t as mcpPlugin } from "./plugin-BZQe1pji.mjs";
4
- import { t as startStdioServer } from "./stdio-37bSPka9.mjs";
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 { C as validateMcpPrincipal, d as getAppMcpRelease, m as assertBoundedMcpValue, t as createMcpServer } from "./create-server-5Eo4QP3q.mjs";
2
- import { t as mcpPlugin } from "./plugin-BZQe1pji.mjs";
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;
@@ -1,2 +1,2 @@
1
- import { n as mcpModule, t as McpModule } from "../index-C3gm6ar5.mjs";
1
+ import { n as mcpModule, t as McpModule } from "../index-CLEEqMqK.mjs";
2
2
  export { type McpModule, mcpModule };
@@ -1,3 +1,3 @@
1
- import { t as mcpModule } from "../mcp-i0Kqu_bB.mjs";
1
+ import { t as mcpModule } from "../mcp-C55EQ-yd.mjs";
2
2
 
3
3
  export { mcpModule };
@@ -5,15 +5,26 @@ function mcpPlugin() {
5
5
  targets: { server: {
6
6
  root: ".",
7
7
  outputFile: "index.ts",
8
- categories: { mcpTools: {
9
- dirs: ["mcp-tools"],
10
- prefix: "mcpTool",
11
- factoryFunctions: ["mcpTool"],
12
- registryKey: true,
13
- includeInAppState: true,
14
- extractFromModules: true,
15
- typeEmit: "standard"
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,3 +1,3 @@
1
- import { t as mcpPlugin } from "./plugin-BZQe1pji.mjs";
1
+ import { t as mcpPlugin } from "./plugin-BgwWI9S8.mjs";
2
2
 
3
3
  export { mcpPlugin };
@@ -1,4 +1,4 @@
1
- import { t as createMcpServer } from "./create-server-5Eo4QP3q.mjs";
1
+ import { t as createMcpServer } from "./create-server-DzTo-3Wy.mjs";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
 
4
4
  //#region src/server/stdio.ts
@@ -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 { McpWorkloadAuthorizer as A, WorkloadMcpServerOptions as B, McpToolConfig as C, McpWorkloadAuditEvent as D, McpTransportKind as E, McpWorkloadHandoffInput as F, McpWorkloadRequirement as I, McpWorkloadToolFacts as L, McpWorkloadContextBindingInput as M, McpWorkloadExecutionControl as N, McpWorkloadAuthorization as O, McpWorkloadHandoff as P, McpWorkloadToolKind as R, McpStdioConfig as S, McpToolHandlerArgs as T, McpProgrammaticToolResult as _, McpAccessRule as a, McpResourcesConfig as b, McpCrudConfig as c, McpExecutionDiagnosticEvent as d, McpExecutionLimits as f, McpProgrammaticTool as g, McpProgrammaticRequestOptions as h, McpAccessMode as i, McpWorkloadContextBinder as j, McpWorkloadAuthorizationRequest 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, McpPublicErrorCode as v, McpToolDefinition as w, McpRoutesConfig as x, McpRequiredScopes as y, McpWorkloadToolPort as z };
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-C3663EKG.mjs";
1
+ import { t as startStdioServer } from "./stdio-C2V13-vP.mjs";
2
2
  export { startStdioServer };
package/dist/stdio.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as startStdioServer } from "./stdio-37bSPka9.mjs";
1
+ import { t as startStdioServer } from "./stdio-Bq7nzR79.mjs";
2
2
 
3
3
  export { startStdioServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@questpie/mcp",
3
- "version": "3.28.11",
3
+ "version": "3.30.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.28.11",
73
+ "questpie": "^3.30.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.28.11"
83
+ "questpie": "^3.30.0"
84
84
  }
85
85
  }