@absolutejs/mcp 0.4.3 → 0.5.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
@@ -7,6 +7,49 @@ package owns the JSON-RPC protocol, protocol-version negotiation, RFC 9728
7
7
  discovery metadata, and the `401` challenge that lets a client find your
8
8
  authorization server.
9
9
 
10
+ ## Agent action enforcement
11
+
12
+ Tools carrying manifest contract 2 `authorization` metadata fail closed unless
13
+ an `agency` enforcement point is configured. Every call becomes an exact-input
14
+ action request; allowed calls execute through a short-lived single-use lease and
15
+ produce a receipt. Requestable denials return an `absolute.action_decision`
16
+ payload containing the action id for an approval workflow.
17
+
18
+ ```ts
19
+ import { createAgency, createMemoryAgencyStore } from "@absolutejs/agency";
20
+
21
+ const agency = createAgency({ policy, store: createMemoryAgencyStore() });
22
+
23
+ mcpServer<Caller>({
24
+ agency: {
25
+ enforcement: agency,
26
+ resolveActor: ({ caller, scopes }) => ({
27
+ agentId: caller.agentId,
28
+ delegationId: caller.delegationId,
29
+ scopes,
30
+ userId: caller.userId,
31
+ }),
32
+ },
33
+ // normal MCP config…
34
+ });
35
+ ```
36
+
37
+ ## Durable Tasks
38
+
39
+ The package implements the final `io.modelcontextprotocol/tasks` extension from
40
+ SEP-2663: server-directed task creation, `tasks/get`, `tasks/update`, and
41
+ `tasks/cancel`. There is intentionally no `tasks/list`; task handles are bound
42
+ to an authorization key and checked on every request.
43
+
44
+ ```ts
45
+ tasks: {
46
+ authorizationKey: (caller) => caller.userId,
47
+ shouldCreate: ({ name }) => name === "long_running_report",
48
+ store: createMemoryMcpTaskStore(), // use a durable shared store in production
49
+ ttlMs: 60 * 60 * 1000,
50
+ }
51
+ ```
52
+
10
53
  Nothing here depends on a model. The tool shape is structurally compatible with
11
54
  [`@absolutejs/ai`](https://github.com/absolutejs/ai)'s `AIToolMap`, so an AI tool
12
55
  registry serves over MCP without conversion — but any typed tool registry works.
package/dist/index.js CHANGED
@@ -285,12 +285,32 @@ var createMcpClient = (options) => {
285
285
  };
286
286
  return { callTool, initialize, listResources, listTools, ping, readResource };
287
287
  };
288
+ // node_modules/@absolutejs/agency/dist/authzen.js
289
+ var createCoazActionInput = ({
290
+ actor,
291
+ call,
292
+ effects = ["read"],
293
+ requiredScopes = []
294
+ }) => ({
295
+ action: "call_tool",
296
+ actor,
297
+ context: { required_scopes: requiredScopes, source: "mcp" },
298
+ effects,
299
+ input: call.arguments,
300
+ resource: {
301
+ id: call.name,
302
+ properties: { server_id: call.serverId },
303
+ type: "mcp_tool"
304
+ }
305
+ });
306
+
288
307
  // src/jsonrpc.ts
289
308
  var JSONRPC_PARSE_ERROR = -32700;
290
309
  var JSONRPC_INVALID_REQUEST = -32600;
291
310
  var JSONRPC_METHOD_NOT_FOUND = -32601;
292
311
  var JSONRPC_INVALID_PARAMS = -32602;
293
312
  var JSONRPC_INTERNAL_ERROR = -32603;
313
+ var JSONRPC_MISSING_REQUIRED_CLIENT_CAPABILITY = -32003;
294
314
  var HTTP_ACCEPTED = 202;
295
315
  var HTTP_NO_CONTENT = 204;
296
316
  var HTTP_UNAUTHORIZED = 401;
@@ -301,7 +321,11 @@ var jsonHeaders = {
301
321
  var rpcResult = (id, result) => new Response(JSON.stringify({ id, jsonrpc: "2.0", result }), {
302
322
  headers: jsonHeaders
303
323
  });
304
- var rpcError = (id, code, message) => new Response(JSON.stringify({ error: { code, message }, id, jsonrpc: "2.0" }), {
324
+ var rpcError = (id, code, message, data) => new Response(JSON.stringify({
325
+ error: { code, message, ...data === undefined ? {} : { data } },
326
+ id,
327
+ jsonrpc: "2.0"
328
+ }), {
305
329
  headers: jsonHeaders
306
330
  });
307
331
  var notificationAck = () => new Response(null, { status: HTTP_ACCEPTED });
@@ -317,6 +341,49 @@ var unauthorized = (metadataUrl, detail) => new Response(JSON.stringify({
317
341
  status: HTTP_UNAUTHORIZED
318
342
  });
319
343
 
344
+ // src/tasks.ts
345
+ var clone = (value) => structuredClone(value);
346
+ var createMemoryMcpTaskStore = () => {
347
+ const tasks = new Map;
348
+ return {
349
+ cancel: (taskId) => {
350
+ const task = tasks.get(taskId);
351
+ if (task === undefined)
352
+ return;
353
+ tasks.set(taskId, {
354
+ ...task,
355
+ lastUpdatedAt: new Date().toISOString(),
356
+ status: "cancelled"
357
+ });
358
+ },
359
+ get: (taskId) => {
360
+ const task = tasks.get(taskId);
361
+ return task === undefined ? null : clone(task);
362
+ },
363
+ save: (task) => {
364
+ tasks.set(task.taskId, clone(task));
365
+ },
366
+ update: (taskId, update) => {
367
+ const task = tasks.get(taskId);
368
+ if (task === undefined)
369
+ return null;
370
+ if (["cancelled", "completed", "failed"].includes(task.status)) {
371
+ return clone(task);
372
+ }
373
+ const next = {
374
+ ...task,
375
+ ...update,
376
+ lastUpdatedAt: new Date().toISOString()
377
+ };
378
+ tasks.set(taskId, next);
379
+ return clone(next);
380
+ }
381
+ };
382
+ };
383
+ var publicMcpTask = ({ authorizationKey, ...task }) => {
384
+ return task;
385
+ };
386
+
320
387
  // src/dispatch.ts
321
388
  var DEFAULT_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
322
389
  var DEFAULT_RESOURCE_MIME = "text/markdown";
@@ -347,6 +414,16 @@ var negotiateProtocol = (supported, params) => {
347
414
  return supported.includes(requested) ? requested : preferred;
348
415
  };
349
416
  var scopeAllows = (tool, scopes) => tool.scope === undefined || scopes.includes(tool.scope);
417
+ var valueAtPath = (value, path) => {
418
+ let current = value;
419
+ for (const segment of path.split(".")) {
420
+ if (!isRecord(current))
421
+ return;
422
+ current = current[segment];
423
+ }
424
+ return current;
425
+ };
426
+ var agencyAllows = (config, tool, scopes) => tool.authorization === undefined || config.agency !== undefined && (tool.authorization.requiredScopes ?? []).every((scope) => scopes.includes(scope));
350
427
  var normalizeResult = (value) => {
351
428
  if (typeof value === "string") {
352
429
  return { content: [{ text: value, type: "text" }], isError: false };
@@ -365,6 +442,9 @@ var initialize = async (config, id, params, context) => {
365
442
  const capabilities = {
366
443
  tools: { listChanged: false }
367
444
  };
445
+ if (config.tasks !== undefined) {
446
+ capabilities.extensions = { "io.modelcontextprotocol/tasks": {} };
447
+ }
368
448
  if (config.prompts)
369
449
  capabilities.prompts = { listChanged: false };
370
450
  if (config.resources) {
@@ -384,7 +464,7 @@ var initialize = async (config, id, params, context) => {
384
464
  };
385
465
  var toolsList = async (config, caller, scopes, id, params) => {
386
466
  const tools = await config.tools({ caller, meta: {} });
387
- const visible = Object.entries(tools).filter(([, tool]) => scopeAllows(tool, scopes)).map(([name, tool]) => ({
467
+ const visible = Object.entries(tools).filter(([, tool]) => scopeAllows(tool, scopes) && agencyAllows(config, tool, scopes)).map(([name, tool]) => ({
388
468
  annotations: tool.annotations,
389
469
  description: tool.description,
390
470
  inputSchema: tool.inputSchema,
@@ -411,11 +491,70 @@ var SSE_HEADERS = {
411
491
  var sseFrame = (message) => `data: ${JSON.stringify(message)}
412
492
 
413
493
  `;
414
- var runTool = async (config, caller, id, name, args, meta, tool, context) => {
494
+ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context) => {
415
495
  let ok = false;
416
496
  let payload;
417
497
  try {
418
- const result = normalizeResult(await tool.handler(args, context));
498
+ const invoke = async () => normalizeResult(await tool.handler(args, context));
499
+ let result;
500
+ if (tool.authorization === undefined) {
501
+ result = await invoke();
502
+ } else {
503
+ const agency = config.agency;
504
+ if (agency === undefined)
505
+ throw new Error("Agent action policy is not configured");
506
+ const actor = await agency.resolveActor({ caller, scopes });
507
+ const actionInput = createCoazActionInput({
508
+ actor,
509
+ call: {
510
+ arguments: args,
511
+ name,
512
+ serverId: agency.serverId ?? config.serverInfo.name
513
+ },
514
+ effects: tool.authorization.effects,
515
+ requiredScopes: tool.authorization.requiredScopes
516
+ });
517
+ const amount = tool.authorization.spend ? valueAtPath(args, tool.authorization.spend.amountMinorField) : undefined;
518
+ const currency = tool.authorization.spend ? valueAtPath(args, tool.authorization.spend.currencyField) : undefined;
519
+ const idempotencyKey = tool.authorization.idempotencyKeyField ? valueAtPath(args, tool.authorization.idempotencyKeyField) : undefined;
520
+ const requested = await agency.enforcement.request({
521
+ ...actionInput,
522
+ context: {
523
+ ...actionInput.context,
524
+ manifest_authorization: tool.authorization
525
+ },
526
+ idempotencyKey: typeof idempotencyKey === "string" ? idempotencyKey : undefined,
527
+ spend: typeof amount === "number" && typeof currency === "string" ? { amountMinor: amount, currency } : undefined
528
+ });
529
+ meta.agencyActionId = requested.action.actionId;
530
+ meta.agencyDecisionId = requested.decision.decisionId;
531
+ if (requested.decision.kind === "deny") {
532
+ result = {
533
+ content: [
534
+ {
535
+ text: requested.decision.requestable ? `Action requires approval (${requested.action.actionId})` : `Action denied: ${requested.decision.reason}`,
536
+ type: "text"
537
+ }
538
+ ],
539
+ isError: true,
540
+ structuredContent: {
541
+ actionId: requested.action.actionId,
542
+ decision: requested.decision,
543
+ type: "absolute.action_decision"
544
+ }
545
+ };
546
+ } else {
547
+ const lease = await agency.enforcement.issueLease(requested.action.actionId);
548
+ const executed = await agency.enforcement.execute({
549
+ executor: `mcp:${config.serverInfo.name}/${name}`,
550
+ leaseId: lease.leaseId,
551
+ run: invoke
552
+ });
553
+ meta.agencyLeaseId = lease.leaseId;
554
+ meta.agencyReceiptId = executed.receipt.receiptId;
555
+ result = executed.result;
556
+ }
557
+ }
419
558
  ok = result.isError !== true;
420
559
  payload = { id, jsonrpc: "2.0", result };
421
560
  } catch (error) {
@@ -433,7 +572,7 @@ var runTool = async (config, caller, id, name, args, meta, tool, context) => {
433
572
  await config.onCall({ args, caller, meta, name, ok });
434
573
  return payload;
435
574
  };
436
- var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions, canElicit) => {
575
+ var toolsCallStreaming = (config, caller, scopes, id, name, args, meta, tool, sessions, canElicit) => {
437
576
  const encoder = new TextEncoder;
438
577
  const body = new ReadableStream({
439
578
  async start(controller) {
@@ -462,7 +601,7 @@ var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions,
462
601
  return await pending.answer;
463
602
  }
464
603
  };
465
- const payload = await runTool(config, caller, id, name, args, meta, tool, context);
604
+ const payload = await runTool(config, caller, scopes, id, name, args, meta, tool, context);
466
605
  send(payload);
467
606
  if (open) {
468
607
  try {
@@ -489,19 +628,101 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
489
628
  }
490
629
  const tools = await config.tools({ caller, meta });
491
630
  const tool = tools[name];
492
- if (!tool || !scopeAllows(tool, scopes)) {
631
+ if (!tool || !scopeAllows(tool, scopes) || !agencyAllows(config, tool, scopes)) {
493
632
  return rpcError(id, JSONRPC_INVALID_PARAMS, `Unknown tool: ${name}`);
494
633
  }
634
+ const tasks = config.tasks;
635
+ if (tasks !== undefined && await tasks.shouldCreate({ args, caller, name })) {
636
+ if (!supportsTasks(params)) {
637
+ return rpcError(id, JSONRPC_MISSING_REQUIRED_CLIENT_CAPABILITY, "Missing required client capability", {
638
+ requiredCapabilities: {
639
+ extensions: { "io.modelcontextprotocol/tasks": {} }
640
+ }
641
+ });
642
+ }
643
+ const createdAt = new Date().toISOString();
644
+ const task = {
645
+ authorizationKey: await tasks.authorizationKey(caller),
646
+ createdAt,
647
+ lastUpdatedAt: createdAt,
648
+ pollIntervalMs: tasks.pollIntervalMs,
649
+ status: "working",
650
+ taskId: crypto.randomUUID(),
651
+ ttlMs: tasks.ttlMs ?? null
652
+ };
653
+ await tasks.store.save(task);
654
+ setTimeout(() => {
655
+ runTool(config, caller, scopes, id, name, args, meta, tool, noElicit).then(async (payload2) => {
656
+ const result = isRecord(payload2) && isRecord(payload2.result) ? payload2.result : { content: [], isError: true };
657
+ await tasks.store.update(task.taskId, {
658
+ result,
659
+ status: "completed"
660
+ });
661
+ }).catch(async (error) => {
662
+ await tasks.store.update(task.taskId, {
663
+ error: {
664
+ code: JSONRPC_INTERNAL_ERROR,
665
+ message: error instanceof Error ? error.message : "Task failed"
666
+ },
667
+ status: "failed"
668
+ });
669
+ });
670
+ }, 0);
671
+ return rpcResult(id, { ...publicMcpTask(task), resultType: "task" });
672
+ }
495
673
  const sessions = context.sessions;
496
674
  const session = sessions ? await sessions.get(context.sessionId ?? null) : null;
497
675
  if (tool.mayElicit === true && config.elicitation?.enabled === true && sessions && session) {
498
- return toolsCallStreaming(config, caller, id, name, args, meta, tool, sessions, session.canElicit);
676
+ return toolsCallStreaming(config, caller, scopes, id, name, args, meta, tool, sessions, session.canElicit);
499
677
  }
500
- const payload = await runTool(config, caller, id, name, args, meta, tool, noElicit);
678
+ const payload = await runTool(config, caller, scopes, id, name, args, meta, tool, noElicit);
501
679
  return new Response(JSON.stringify(payload), {
502
680
  headers: { "content-type": "application/json" }
503
681
  });
504
682
  };
683
+ var supportsTasks = (params) => {
684
+ if (!isRecord(params) || !isRecord(params._meta))
685
+ return false;
686
+ const capabilities = params._meta["io.modelcontextprotocol/clientCapabilities"];
687
+ if (!isRecord(capabilities) || !isRecord(capabilities.extensions))
688
+ return false;
689
+ return isRecord(capabilities.extensions["io.modelcontextprotocol/tasks"]);
690
+ };
691
+ var authorizedTask = async (config, caller, params) => {
692
+ if (config.tasks === undefined || !isRecord(params) || typeof params.taskId !== "string") {
693
+ return null;
694
+ }
695
+ const task = await config.tasks.store.get(params.taskId);
696
+ if (task === null)
697
+ return null;
698
+ const authorizationKey = await config.tasks.authorizationKey(caller);
699
+ return task.authorizationKey === authorizationKey ? task : null;
700
+ };
701
+ var tasksGet = async (config, caller, id, params) => {
702
+ const task = await authorizedTask(config, caller, params);
703
+ if (task === null)
704
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
705
+ return rpcResult(id, { ...publicMcpTask(task), resultType: "complete" });
706
+ };
707
+ var tasksUpdate = async (config, caller, id, params) => {
708
+ const task = await authorizedTask(config, caller, params);
709
+ if (task === null || !isRecord(params) || !isRecord(params.inputResponses)) {
710
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task or invalid inputResponses");
711
+ }
712
+ await config.tasks?.onUpdate?.({
713
+ caller,
714
+ inputResponses: params.inputResponses,
715
+ task
716
+ });
717
+ return rpcResult(id, { resultType: "complete" });
718
+ };
719
+ var tasksCancel = async (config, caller, id, params) => {
720
+ const task = await authorizedTask(config, caller, params);
721
+ if (task === null)
722
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
723
+ await config.tasks?.store.cancel(task.taskId);
724
+ return rpcResult(id, { resultType: "complete" });
725
+ };
505
726
  var promptsList = (config, id, params) => {
506
727
  const definitions = config.prompts?.definitions ?? {};
507
728
  const all = Object.entries(definitions).map(([name, def]) => ({
@@ -593,6 +814,14 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
593
814
  if (method === "initialize") {
594
815
  return await initialize(config, id, params, context);
595
816
  }
817
+ if (method === "server/discover") {
818
+ return rpcResult(id, {
819
+ capabilities: {
820
+ extensions: config.tasks === undefined ? {} : { "io.modelcontextprotocol/tasks": {} }
821
+ },
822
+ serverInfo: config.serverInfo
823
+ });
824
+ }
596
825
  if (method === "ping")
597
826
  return rpcResult(id, {});
598
827
  if (method === "tools/list") {
@@ -601,6 +830,12 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
601
830
  if (method === "tools/call") {
602
831
  return toolsCall(config, caller, scopes, id, params, context);
603
832
  }
833
+ if (method === "tasks/get")
834
+ return tasksGet(config, caller, id, params);
835
+ if (method === "tasks/update")
836
+ return tasksUpdate(config, caller, id, params);
837
+ if (method === "tasks/cancel")
838
+ return tasksCancel(config, caller, id, params);
604
839
  if (method === "prompts/list")
605
840
  return promptsList(config, id, params);
606
841
  if (method === "prompts/get")
@@ -896,12 +1131,14 @@ var mcpServer = (config) => {
896
1131
  };
897
1132
  export {
898
1133
  verifyBearer,
1134
+ publicMcpTask,
899
1135
  protectedResourceMetadata,
900
1136
  metadataPathFor,
901
1137
  mcpServer,
902
1138
  feedbackTools,
903
1139
  dispatchMcp,
904
1140
  createSessionRegistry,
1141
+ createMemoryMcpTaskStore,
905
1142
  createMcpHandler,
906
1143
  createMcpClient,
907
1144
  McpClientError,
package/dist/manifest.js CHANGED
@@ -5883,8 +5883,27 @@ var toolAnnotations = Type.Object({
5883
5883
  readOnlyHint: Type.Optional(Type.Boolean()),
5884
5884
  title: Type.Optional(Type.String())
5885
5885
  });
5886
+ var toolAuthorization = Type.Object({
5887
+ approval: Type.Optional(Type.Union([
5888
+ Type.Literal("always"),
5889
+ Type.Literal("never"),
5890
+ Type.Literal("policy")
5891
+ ])),
5892
+ compensatingTool: Type.Optional(Type.String({ pattern: TOOL_NAME_PATTERN.source })),
5893
+ destinations: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5894
+ effects: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
5895
+ idempotencyKeyField: Type.Optional(Type.String({ minLength: 1 })),
5896
+ requiredScopes: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5897
+ reversible: Type.Optional(Type.Boolean()),
5898
+ spend: Type.Optional(Type.Object({
5899
+ amountMinorField: Type.String({ minLength: 1 }),
5900
+ currencyField: Type.String({ minLength: 1 }),
5901
+ maximumAmountMinor: Type.Optional(Type.Integer({ minimum: 0 }))
5902
+ }))
5903
+ });
5886
5904
  var serializedTool = Type.Object({
5887
5905
  annotations: Type.Optional(toolAnnotations),
5906
+ authorization: Type.Optional(toolAuthorization),
5888
5907
  capabilities: Type.Optional(Type.Array(Type.Union([
5889
5908
  Type.Literal("exec"),
5890
5909
  Type.Literal("glob"),
@@ -5896,7 +5915,7 @@ var serializedTool = Type.Object({
5896
5915
  kind: Type.Union([Type.Literal("runtime"), Type.Literal("workspace")])
5897
5916
  });
5898
5917
  var manifestSchema = Type.Object({
5899
- contract: Type.Literal(1),
5918
+ contract: Type.Union([Type.Literal(1), Type.Literal(2)]),
5900
5919
  identity: Type.Object({
5901
5920
  accent: Type.Optional(Type.String({ pattern: "^#[0-9a-fA-F]{3,8}$" })),
5902
5921
  category: Type.String({ minLength: 1 }),
@@ -8477,11 +8496,11 @@ var Type2 = exports_type6;
8477
8496
 
8478
8497
  // src/manifest.ts
8479
8498
  var manifest = defineManifest()({
8480
- contract: 1,
8499
+ contract: 2,
8481
8500
  identity: {
8482
8501
  accent: "#0ea5e9",
8483
8502
  category: "ai",
8484
- description: "Serve a remote Model Context Protocol endpoint (streamable HTTP, stateless) from your Elysia app. Bridge installed AbsoluteJS packages' manifests with `toMcpToolRegistry` and their tools become callable by any connected MCP client; OAuth bearer auth, RFC 9728 discovery, and per-call guards are yours to wire.",
8503
+ description: "Serve a remote Model Context Protocol endpoint with OAuth discovery, agency action enforcement, approval receipts, and caller-bound durable Tasks. Bridge installed AbsoluteJS packages' manifests with `toMcpToolRegistry` and enforce their declared effects before execution.",
8485
8504
  docsUrl: "https://github.com/absolutejs/mcp",
8486
8505
  name: "@absolutejs/mcp",
8487
8506
  tagline: "Let AI assistants connect to your site and use its tools."
@@ -1,9 +1,9 @@
1
1
  {
2
- "contract": 1,
2
+ "contract": 2,
3
3
  "identity": {
4
4
  "accent": "#0ea5e9",
5
5
  "category": "ai",
6
- "description": "Serve a remote Model Context Protocol endpoint (streamable HTTP, stateless) from your Elysia app. Bridge installed AbsoluteJS packages' manifests with `toMcpToolRegistry` and their tools become callable by any connected MCP client; OAuth bearer auth, RFC 9728 discovery, and per-call guards are yours to wire.",
6
+ "description": "Serve a remote Model Context Protocol endpoint with OAuth discovery, agency action enforcement, approval receipts, and caller-bound durable Tasks. Bridge installed AbsoluteJS packages' manifests with `toMcpToolRegistry` and enforce their declared effects before execution.",
7
7
  "docsUrl": "https://github.com/absolutejs/mcp",
8
8
  "name": "@absolutejs/mcp",
9
9
  "tagline": "Let AI assistants connect to your site and use its tools."
@@ -38,4 +38,5 @@ export { createMcpHandler } from "./handler";
38
38
  export { metadataPathFor, protectedResourceMetadata, type ProtectedResourceMetadata, } from "./metadata";
39
39
  export { mcpServer } from "./server";
40
40
  export { createSessionRegistry, type SessionRegistry } from "./sessions";
41
- export type { McpAudioContent, McpElicitAnswer, McpElicitationRequest, McpElicitBus, McpElicitResult, McpSessionStore, McpAuthResult, McpCallGate, McpCallMeta, McpContent, McpImageContent, McpPromptArgument, McpPromptDefinition, McpPrompts, McpResource, McpResourceLink, McpResources, McpServerConfig, McpServerInfo, McpTextContent, McpTool, McpToolAnnotations, McpToolCallContext, McpToolContext, McpToolRegistry, McpToolResult, McpToolReturn, } from "./types";
41
+ export { createMemoryMcpTaskStore, publicMcpTask } from "./tasks";
42
+ export type { McpAgencyOptions, McpAudioContent, McpElicitAnswer, McpElicitationRequest, McpElicitBus, McpElicitResult, McpSessionStore, McpAuthResult, McpCallGate, McpCallMeta, McpContent, McpImageContent, McpPromptArgument, McpPromptDefinition, McpPrompts, McpResource, McpResourceLink, McpResources, McpServerConfig, McpServerInfo, McpTextContent, McpTask, McpTaskStatus, McpTaskStore, McpTasksOptions, McpTool, McpToolAnnotations, McpToolCallContext, McpToolContext, McpToolRegistry, McpToolResult, McpToolReturn, } from "./types";
@@ -3,13 +3,14 @@ export declare const JSONRPC_INVALID_REQUEST = -32600;
3
3
  export declare const JSONRPC_METHOD_NOT_FOUND = -32601;
4
4
  export declare const JSONRPC_INVALID_PARAMS = -32602;
5
5
  export declare const JSONRPC_INTERNAL_ERROR = -32603;
6
+ export declare const JSONRPC_MISSING_REQUIRED_CLIENT_CAPABILITY = -32003;
6
7
  export declare const HTTP_ACCEPTED = 202;
7
8
  export declare const HTTP_NO_CONTENT = 204;
8
9
  export declare const HTTP_UNAUTHORIZED = 401;
9
10
  export declare const HTTP_METHOD_NOT_ALLOWED = 405;
10
11
  export type JsonRpcId = string | number | null;
11
12
  export declare const rpcResult: (id: JsonRpcId, result: unknown) => Response;
12
- export declare const rpcError: (id: JsonRpcId, code: number, message: string) => Response;
13
+ export declare const rpcError: (id: JsonRpcId, code: number, message: string, data?: Record<string, unknown>) => Response;
13
14
  export declare const notificationAck: () => Response;
14
15
  /** 401 with the RFC 9728 `WWW-Authenticate` challenge pointing at the
15
16
  * protected-resource metadata, so the client can discover the auth server. */
@@ -0,0 +1,14 @@
1
+ import type { McpTask, McpTaskStore } from "./types";
2
+ export declare const createMemoryMcpTaskStore: () => McpTaskStore;
3
+ export declare const publicMcpTask: ({ authorizationKey, ...task }: McpTask) => {
4
+ createdAt: string;
5
+ error?: Record<string, unknown>;
6
+ inputRequests?: Record<string, unknown>;
7
+ lastUpdatedAt: string;
8
+ pollIntervalMs?: number;
9
+ result?: Record<string, unknown>;
10
+ status: import("./types").McpTaskStatus;
11
+ statusMessage?: string;
12
+ taskId: string;
13
+ ttlMs: number | null;
14
+ };
@@ -1,3 +1,5 @@
1
+ import type { Agency, AgentActor } from "@absolutejs/agency";
2
+ import type { ToolAuthorization } from "@absolutejs/manifest";
1
3
  /** MCP behaviour hints, passed straight through to the client on `tools/list`.
2
4
  * Structurally identical to `@absolutejs/ai`'s `AIToolAnnotations`, so a tool
3
5
  * map from that package satisfies this without conversion. All optional. */
@@ -108,6 +110,9 @@ export type McpToolCallContext = {
108
110
  /** One callable tool. `inputSchema` is a JSON Schema object. */
109
111
  export type McpTool = {
110
112
  annotations?: McpToolAnnotations;
113
+ /** Enforceable semantic effects from manifest contract 2. A tool carrying
114
+ * this is hidden unless the server configures `agency`. */
115
+ authorization?: ToolAuthorization;
111
116
  description: string;
112
117
  handler: (args: unknown, context: McpToolCallContext) => McpToolReturn | Promise<McpToolReturn>;
113
118
  inputSchema: Record<string, unknown>;
@@ -165,6 +170,50 @@ export type McpToolContext<Caller> = {
165
170
  caller: Caller;
166
171
  meta: McpCallMeta;
167
172
  };
173
+ export type McpAgencyOptions<Caller> = {
174
+ enforcement: Agency;
175
+ resolveActor: (context: {
176
+ caller: Caller;
177
+ scopes: string[];
178
+ }) => Promise<AgentActor> | AgentActor;
179
+ serverId?: string;
180
+ };
181
+ export type McpTaskStatus = "cancelled" | "completed" | "failed" | "input_required" | "working";
182
+ export type McpTask = {
183
+ authorizationKey: string;
184
+ createdAt: string;
185
+ error?: Record<string, unknown>;
186
+ inputRequests?: Record<string, unknown>;
187
+ lastUpdatedAt: string;
188
+ pollIntervalMs?: number;
189
+ result?: Record<string, unknown>;
190
+ status: McpTaskStatus;
191
+ statusMessage?: string;
192
+ taskId: string;
193
+ ttlMs: number | null;
194
+ };
195
+ export type McpTaskStore = {
196
+ cancel: (taskId: string) => Promise<void> | void;
197
+ get: (taskId: string) => Promise<McpTask | null> | McpTask | null;
198
+ save: (task: McpTask) => Promise<void> | void;
199
+ update: (taskId: string, update: Partial<Omit<McpTask, "authorizationKey" | "createdAt" | "taskId">>) => Promise<McpTask | null> | McpTask | null;
200
+ };
201
+ export type McpTasksOptions<Caller> = {
202
+ authorizationKey: (caller: Caller) => Promise<string> | string;
203
+ onUpdate?: (context: {
204
+ caller: Caller;
205
+ inputResponses: Record<string, unknown>;
206
+ task: McpTask;
207
+ }) => Promise<void> | void;
208
+ pollIntervalMs?: number;
209
+ shouldCreate: (context: {
210
+ args: unknown;
211
+ caller: Caller;
212
+ name: string;
213
+ }) => Promise<boolean> | boolean;
214
+ store: McpTaskStore;
215
+ ttlMs?: number | null;
216
+ };
168
217
  export type McpPrompts<Caller> = {
169
218
  definitions: Record<string, McpPromptDefinition>;
170
219
  get: (ctx: {
@@ -190,6 +239,8 @@ export type McpServerInfo = {
190
239
  version: string;
191
240
  };
192
241
  export type McpServerConfig<Caller> = {
242
+ /** Per-tool action policy enforcement, approval, leases, and receipts. */
243
+ agency?: McpAgencyOptions<Caller>;
193
244
  /** Resolve the request into a caller, or a reason for the 401. The package
194
245
  * emits the 401 + RFC 9728 `WWW-Authenticate` challenge; you decide who is
195
246
  * allowed in. See {@link verifyBearer} for the standard token checks. */
@@ -244,6 +295,8 @@ export type McpServerConfig<Caller> = {
244
295
  /** Protocol versions this endpoint accepts; the first is the preferred one.
245
296
  * Defaults to the versions this package knows. */
246
297
  supportedProtocols?: string[];
298
+ /** Final SEP-2663 `io.modelcontextprotocol/tasks` extension support. */
299
+ tasks?: McpTasksOptions<Caller>;
247
300
  /** Build the tool registry for this caller. Called once per request. */
248
301
  tools: (ctx: McpToolContext<Caller>) => McpToolRegistry | Promise<McpToolRegistry>;
249
302
  };
package/package.json CHANGED
@@ -11,12 +11,13 @@
11
11
  "elysia": ">=1.1.0"
12
12
  },
13
13
  "dependencies": {
14
- "@absolutejs/manifest": "^0.1.0",
14
+ "@absolutejs/agency": "^0.1.0",
15
+ "@absolutejs/manifest": "^0.2.0",
15
16
  "@sinclair/typebox": "^0.34.0"
16
17
  },
17
18
  "license": "BUSL-1.1",
18
19
  "absolutejs": {
19
- "manifestContract": 1
20
+ "manifestContract": 2
20
21
  },
21
22
  "exports": {
22
23
  ".": {
@@ -57,5 +58,5 @@
57
58
  "typecheck": "tsc --noEmit --project tsconfig.json"
58
59
  },
59
60
  "types": "./dist/src/index.d.ts",
60
- "version": "0.4.3"
61
+ "version": "0.5.0"
61
62
  }