@absolutejs/mcp 0.4.2 → 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
@@ -1,4 +1,19 @@
1
1
  // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
2
17
  // src/guards.ts
3
18
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
19
 
@@ -270,12 +285,32 @@ var createMcpClient = (options) => {
270
285
  };
271
286
  return { callTool, initialize, listResources, listTools, ping, readResource };
272
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
+
273
307
  // src/jsonrpc.ts
274
308
  var JSONRPC_PARSE_ERROR = -32700;
275
309
  var JSONRPC_INVALID_REQUEST = -32600;
276
310
  var JSONRPC_METHOD_NOT_FOUND = -32601;
277
311
  var JSONRPC_INVALID_PARAMS = -32602;
278
312
  var JSONRPC_INTERNAL_ERROR = -32603;
313
+ var JSONRPC_MISSING_REQUIRED_CLIENT_CAPABILITY = -32003;
279
314
  var HTTP_ACCEPTED = 202;
280
315
  var HTTP_NO_CONTENT = 204;
281
316
  var HTTP_UNAUTHORIZED = 401;
@@ -286,7 +321,11 @@ var jsonHeaders = {
286
321
  var rpcResult = (id, result) => new Response(JSON.stringify({ id, jsonrpc: "2.0", result }), {
287
322
  headers: jsonHeaders
288
323
  });
289
- 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
+ }), {
290
329
  headers: jsonHeaders
291
330
  });
292
331
  var notificationAck = () => new Response(null, { status: HTTP_ACCEPTED });
@@ -302,6 +341,49 @@ var unauthorized = (metadataUrl, detail) => new Response(JSON.stringify({
302
341
  status: HTTP_UNAUTHORIZED
303
342
  });
304
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
+
305
387
  // src/dispatch.ts
306
388
  var DEFAULT_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
307
389
  var DEFAULT_RESOURCE_MIME = "text/markdown";
@@ -332,6 +414,16 @@ var negotiateProtocol = (supported, params) => {
332
414
  return supported.includes(requested) ? requested : preferred;
333
415
  };
334
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));
335
427
  var normalizeResult = (value) => {
336
428
  if (typeof value === "string") {
337
429
  return { content: [{ text: value, type: "text" }], isError: false };
@@ -350,6 +442,9 @@ var initialize = async (config, id, params, context) => {
350
442
  const capabilities = {
351
443
  tools: { listChanged: false }
352
444
  };
445
+ if (config.tasks !== undefined) {
446
+ capabilities.extensions = { "io.modelcontextprotocol/tasks": {} };
447
+ }
353
448
  if (config.prompts)
354
449
  capabilities.prompts = { listChanged: false };
355
450
  if (config.resources) {
@@ -369,7 +464,7 @@ var initialize = async (config, id, params, context) => {
369
464
  };
370
465
  var toolsList = async (config, caller, scopes, id, params) => {
371
466
  const tools = await config.tools({ caller, meta: {} });
372
- 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]) => ({
373
468
  annotations: tool.annotations,
374
469
  description: tool.description,
375
470
  inputSchema: tool.inputSchema,
@@ -396,11 +491,70 @@ var SSE_HEADERS = {
396
491
  var sseFrame = (message) => `data: ${JSON.stringify(message)}
397
492
 
398
493
  `;
399
- var runTool = async (config, caller, id, name, args, meta, tool, context) => {
494
+ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context) => {
400
495
  let ok = false;
401
496
  let payload;
402
497
  try {
403
- 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
+ }
404
558
  ok = result.isError !== true;
405
559
  payload = { id, jsonrpc: "2.0", result };
406
560
  } catch (error) {
@@ -418,7 +572,7 @@ var runTool = async (config, caller, id, name, args, meta, tool, context) => {
418
572
  await config.onCall({ args, caller, meta, name, ok });
419
573
  return payload;
420
574
  };
421
- var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions, canElicit) => {
575
+ var toolsCallStreaming = (config, caller, scopes, id, name, args, meta, tool, sessions, canElicit) => {
422
576
  const encoder = new TextEncoder;
423
577
  const body = new ReadableStream({
424
578
  async start(controller) {
@@ -447,7 +601,7 @@ var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions,
447
601
  return await pending.answer;
448
602
  }
449
603
  };
450
- 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);
451
605
  send(payload);
452
606
  if (open) {
453
607
  try {
@@ -474,19 +628,101 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
474
628
  }
475
629
  const tools = await config.tools({ caller, meta });
476
630
  const tool = tools[name];
477
- if (!tool || !scopeAllows(tool, scopes)) {
631
+ if (!tool || !scopeAllows(tool, scopes) || !agencyAllows(config, tool, scopes)) {
478
632
  return rpcError(id, JSONRPC_INVALID_PARAMS, `Unknown tool: ${name}`);
479
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
+ }
480
673
  const sessions = context.sessions;
481
674
  const session = sessions ? await sessions.get(context.sessionId ?? null) : null;
482
675
  if (tool.mayElicit === true && config.elicitation?.enabled === true && sessions && session) {
483
- 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);
484
677
  }
485
- 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);
486
679
  return new Response(JSON.stringify(payload), {
487
680
  headers: { "content-type": "application/json" }
488
681
  });
489
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
+ };
490
726
  var promptsList = (config, id, params) => {
491
727
  const definitions = config.prompts?.definitions ?? {};
492
728
  const all = Object.entries(definitions).map(([name, def]) => ({
@@ -578,6 +814,14 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
578
814
  if (method === "initialize") {
579
815
  return await initialize(config, id, params, context);
580
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
+ }
581
825
  if (method === "ping")
582
826
  return rpcResult(id, {});
583
827
  if (method === "tools/list") {
@@ -586,6 +830,12 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
586
830
  if (method === "tools/call") {
587
831
  return toolsCall(config, caller, scopes, id, params, context);
588
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);
589
839
  if (method === "prompts/list")
590
840
  return promptsList(config, id, params);
591
841
  if (method === "prompts/get")
@@ -881,12 +1131,14 @@ var mcpServer = (config) => {
881
1131
  };
882
1132
  export {
883
1133
  verifyBearer,
1134
+ publicMcpTask,
884
1135
  protectedResourceMetadata,
885
1136
  metadataPathFor,
886
1137
  mcpServer,
887
1138
  feedbackTools,
888
1139
  dispatchMcp,
889
1140
  createSessionRegistry,
1141
+ createMemoryMcpTaskStore,
890
1142
  createMcpHandler,
891
1143
  createMcpClient,
892
1144
  McpClientError,