@absolutejs/mcp 0.8.0 → 0.10.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
@@ -1,11 +1,17 @@
1
1
  # @absolutejs/mcp
2
2
 
3
+ MCP tool discovery preserves the OpenID AuthZEN COAZ `coaz` marker and
4
+ `x-coaz-mapping` JSON Schema extension end to end. Use `@absolutejs/policy` to
5
+ validate and evaluate the mapping before dispatching an authorized tool call.
6
+
3
7
  Serve a remote [Model Context Protocol](https://modelcontextprotocol.io) endpoint
4
8
  — streamable HTTP, stateless — from a tool/prompt/resource registry. You supply
5
9
  **which** tools to expose and **how** to authorize a request into a caller; the
6
10
  package owns the JSON-RPC protocol, protocol-version negotiation, RFC 9728
7
11
  discovery metadata, and the `401` challenge that lets a client find your
8
- authorization server.
12
+ authorization server. The default negotiated revision is the current finalized
13
+ `2025-11-25` specification; older finalized revisions remain available when
14
+ explicitly requested.
9
15
 
10
16
  ## Agent action enforcement
11
17
 
@@ -36,10 +42,11 @@ mcpServer<Caller>({
36
42
 
37
43
  ## Durable Tasks
38
44
 
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.
45
+ The package implements native MCP `2025-11-25` task augmentation:
46
+ `execution.taskSupport`, client-requested task creation, `tasks/get`,
47
+ `tasks/result`, authorization-bound `tasks/list`, and terminal-safe
48
+ `tasks/cancel`. It also retains the older `io.modelcontextprotocol/tasks`
49
+ SEP-2663 wire shape only when an older protocol revision is negotiated.
43
50
 
44
51
  ```ts
45
52
  tasks: {
@@ -48,8 +55,20 @@ tasks: {
48
55
  store: createMemoryMcpTaskStore(), // use a durable shared store in production
49
56
  ttlMs: 60 * 60 * 1000,
50
57
  }
58
+
59
+ tools: () => ({
60
+ long_running_report: {
61
+ taskSupport: "optional", // "required" and "forbidden" are also supported
62
+ // normal tool definition…
63
+ },
64
+ })
51
65
  ```
52
66
 
67
+ Clients can use `callToolAsTask`, then `getTask`, `listTasks`, `cancelTask`, and
68
+ `getTaskResult`. Task status never exposes the stored result or authorization
69
+ key; the final result is returned only by `tasks/result` with required
70
+ `io.modelcontextprotocol/related-task` metadata.
71
+
53
72
  For multi-instance production deployments, use
54
73
  `createPostgresMcpTaskStore()` and `createPostgresMcpSessionStore()` after
55
74
  applying `mcpPostgresSchemaSql()`. Task updates and cancellation protect
@@ -224,6 +243,13 @@ dismissed it), or `unsupported` (this client can't ask anyone — check
224
243
  `canElicit` and take another path). Never fabricate an answer for the user; the
225
244
  spec also forbids eliciting **sensitive information**.
226
245
 
246
+ For credentials, third-party OAuth, or payment flows, use `mode: "url"` with a
247
+ unique `elicitationId` and HTTPS URL. The client advertises form and URL modes
248
+ separately, never prefetches the URL, and returns only the user's consent—not
249
+ credentials or page contents. Tool handlers can check `canElicitUrl` before
250
+ starting the flow. The server rejects non-HTTPS URLs except localhost
251
+ development URLs and rejects URLs containing embedded credentials.
252
+
227
253
  **The trade-off, stated plainly.** Elicitation is the one MCP feature a
228
254
  stateless server cannot do: the question goes out on the SSE stream of an
229
255
  in-flight `tools/call`, and the client answers on a _separate_ HTTP POST. Two
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ var verifyBearer = async (config) => {
46
46
  };
47
47
  // src/client.ts
48
48
  var DEFAULT_TIMEOUT_MS = 30000;
49
- var DEFAULT_PROTOCOL = "2025-06-18";
49
+ var DEFAULT_PROTOCOL = "2025-11-25";
50
50
 
51
51
  class McpClientError extends Error {
52
52
  code;
@@ -152,12 +152,19 @@ var createMcpClient = (options) => {
152
152
  const answerServer = async (message) => {
153
153
  if (message.method !== "elicitation/create")
154
154
  return;
155
- const request = isRecord(message.params) ? {
156
- message: typeof message.params.message === "string" ? message.params.message : "",
157
- requestedSchema: isRecord(message.params.requestedSchema) ? message.params.requestedSchema : {}
158
- } : { message: "", requestedSchema: {} };
155
+ const params = isRecord(message.params) ? message.params : {};
156
+ const request = params.mode === "url" && typeof params.elicitationId === "string" && typeof params.url === "string" ? {
157
+ elicitationId: params.elicitationId,
158
+ message: typeof params.message === "string" ? params.message : "",
159
+ mode: "url",
160
+ url: params.url
161
+ } : {
162
+ message: typeof params.message === "string" ? params.message : "",
163
+ mode: "form",
164
+ requestedSchema: isRecord(params.requestedSchema) ? params.requestedSchema : {}
165
+ };
159
166
  const result = options.onElicit ? await options.onElicit(request) : { action: "decline" };
160
- await respond(message.id, result);
167
+ await respond(message.id, request.mode === "url" && result.action === "accept" ? { action: "accept" } : result);
161
168
  };
162
169
  const rpc = async (method, params) => {
163
170
  const controller = new AbortController;
@@ -243,7 +250,7 @@ var createMcpClient = (options) => {
243
250
  };
244
251
  const initialize = async () => {
245
252
  const result = await rpc("initialize", {
246
- capabilities: options.onElicit ? { elicitation: {} } : {},
253
+ capabilities: options.onElicit ? { elicitation: { form: {}, url: {} } } : {},
247
254
  clientInfo: options.clientInfo ?? {
248
255
  name: "@absolutejs/mcp",
249
256
  version: "0"
@@ -265,10 +272,12 @@ var createMcpClient = (options) => {
265
272
  const tools = isRecord(result) && Array.isArray(result.tools) ? result.tools : [];
266
273
  collected.push(...tools.filter(isRecord).map((tool) => ({
267
274
  annotations: isRecord(tool.annotations) ? tool.annotations : undefined,
275
+ coaz: typeof tool.coaz === "boolean" ? tool.coaz : undefined,
268
276
  description: typeof tool.description === "string" ? tool.description : undefined,
269
277
  inputSchema: isRecord(tool.inputSchema) ? tool.inputSchema : undefined,
270
278
  name: typeof tool.name === "string" ? tool.name : "",
271
- outputSchema: isRecord(tool.outputSchema) ? tool.outputSchema : undefined
279
+ outputSchema: isRecord(tool.outputSchema) ? tool.outputSchema : undefined,
280
+ taskSupport: isRecord(tool.execution) && (tool.execution.taskSupport === "forbidden" || tool.execution.taskSupport === "optional" || tool.execution.taskSupport === "required") ? tool.execution.taskSupport : undefined
272
281
  })));
273
282
  const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
274
283
  if (next === undefined)
@@ -284,6 +293,44 @@ var createMcpClient = (options) => {
284
293
  }
285
294
  return { content: [], isError: false };
286
295
  };
296
+ const taskFrom = (value) => {
297
+ if (!isRecord(value) || typeof value.taskId !== "string") {
298
+ throw new McpClientError("Malformed MCP task response");
299
+ }
300
+ return value;
301
+ };
302
+ const callToolAsTask = async (name, args, options2 = {}) => {
303
+ const result = await rpc("tools/call", {
304
+ arguments: args ?? {},
305
+ name,
306
+ task: options2.ttl === undefined ? {} : { ttl: options2.ttl }
307
+ });
308
+ return taskFrom(isRecord(result) ? result.task : undefined);
309
+ };
310
+ const getTask = async (taskId) => taskFrom(await rpc("tasks/get", { taskId }));
311
+ const cancelTask = async (taskId) => taskFrom(await rpc("tasks/cancel", { taskId }));
312
+ const getTaskResult = async (taskId) => {
313
+ const result = await rpc("tasks/result", { taskId });
314
+ if (!isRecord(result) || !Array.isArray(result.content)) {
315
+ throw new McpClientError("Malformed MCP task result");
316
+ }
317
+ return result;
318
+ };
319
+ const listTasks = async () => {
320
+ const collected = [];
321
+ let cursor;
322
+ for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
323
+ const result = await rpc("tasks/list", cursor === undefined ? undefined : { cursor });
324
+ if (isRecord(result) && Array.isArray(result.tasks)) {
325
+ collected.push(...result.tasks.map(taskFrom));
326
+ }
327
+ const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
328
+ if (next === undefined)
329
+ break;
330
+ cursor = next;
331
+ }
332
+ return collected;
333
+ };
287
334
  const listResources = async () => {
288
335
  const collected = [];
289
336
  let cursor;
@@ -303,7 +350,19 @@ var createMcpClient = (options) => {
303
350
  const ping = async () => {
304
351
  await rpc("ping");
305
352
  };
306
- return { callTool, initialize, listResources, listTools, ping, readResource };
353
+ return {
354
+ callTool,
355
+ callToolAsTask,
356
+ cancelTask,
357
+ getTask,
358
+ getTaskResult,
359
+ initialize,
360
+ listResources,
361
+ listTasks,
362
+ listTools,
363
+ ping,
364
+ readResource
365
+ };
307
366
  };
308
367
  // src/oauth.ts
309
368
  var splitChallenges = (value) => {
@@ -657,6 +716,7 @@ var createMemoryMcpTaskStore = () => {
657
716
  const task = tasks.get(taskId);
658
717
  return task === undefined ? null : clone(task);
659
718
  },
719
+ list: (authorizationKey, { limit, offset }) => [...tasks.values()].filter((task) => task.authorizationKey === authorizationKey).sort((left, right) => right.lastUpdatedAt.localeCompare(left.lastUpdatedAt)).slice(offset, offset + limit).map(clone),
660
720
  save: (task) => {
661
721
  tasks.set(task.taskId, clone(task));
662
722
  },
@@ -678,11 +738,22 @@ var createMemoryMcpTaskStore = () => {
678
738
  };
679
739
  };
680
740
  var publicMcpTask = ({ authorizationKey, ...task }) => {
681
- return task;
741
+ const { error, inputRequests, pollIntervalMs, result, ttlMs, ...rest } = task;
742
+ return {
743
+ ...rest,
744
+ ...pollIntervalMs === undefined ? {} : { pollInterval: pollIntervalMs },
745
+ ttl: ttlMs
746
+ };
682
747
  };
683
748
 
684
749
  // src/dispatch.ts
685
- var DEFAULT_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
750
+ var MCP_LATEST_PROTOCOL_VERSION = "2025-11-25";
751
+ var DEFAULT_PROTOCOLS = [
752
+ MCP_LATEST_PROTOCOL_VERSION,
753
+ "2025-06-18",
754
+ "2025-03-26",
755
+ "2024-11-05"
756
+ ];
686
757
  var DEFAULT_RESOURCE_MIME = "text/markdown";
687
758
  var DEFAULT_LIST_PAGE_SIZE = 50;
688
759
  var decodeCursor = (params) => {
@@ -729,18 +800,34 @@ var normalizeResult = (value) => {
729
800
  return { content: value, isError: false };
730
801
  return { isError: false, ...value };
731
802
  };
732
- var clientCanElicit = (params) => {
733
- if (!isRecord(params) || !isRecord(params.capabilities))
734
- return false;
735
- return isRecord(params.capabilities.elicitation);
803
+ var clientElicitation = (params) => {
804
+ if (!isRecord(params) || !isRecord(params.capabilities)) {
805
+ return { form: false, url: false };
806
+ }
807
+ const elicitation = params.capabilities.elicitation;
808
+ if (!isRecord(elicitation))
809
+ return { form: false, url: false };
810
+ return {
811
+ form: Object.keys(elicitation).length === 0 || isRecord(elicitation.form),
812
+ url: isRecord(elicitation.url)
813
+ };
736
814
  };
737
815
  var initialize = async (config, id, params, context) => {
738
816
  const supported = config.supportedProtocols ?? DEFAULT_PROTOCOLS;
817
+ const protocolVersion = negotiateProtocol(supported, params);
739
818
  const capabilities = {
740
819
  tools: { listChanged: false }
741
820
  };
742
821
  if (config.tasks !== undefined) {
743
- capabilities.extensions = { "io.modelcontextprotocol/tasks": {} };
822
+ if (protocolVersion === MCP_LATEST_PROTOCOL_VERSION) {
823
+ capabilities.tasks = {
824
+ cancel: {},
825
+ list: {},
826
+ requests: { tools: { call: {} } }
827
+ };
828
+ } else {
829
+ capabilities.extensions = { "io.modelcontextprotocol/tasks": {} };
830
+ }
744
831
  }
745
832
  if (config.prompts)
746
833
  capabilities.prompts = { listChanged: false };
@@ -750,23 +837,26 @@ var initialize = async (config, id, params, context) => {
750
837
  const response = rpcResult(id, {
751
838
  capabilities,
752
839
  ...config.instructions === undefined ? {} : { instructions: config.instructions },
753
- protocolVersion: negotiateProtocol(supported, params),
840
+ protocolVersion,
754
841
  serverInfo: config.serverInfo
755
842
  });
756
843
  if (!config.elicitation?.enabled || !context.sessions)
757
844
  return response;
758
- const sessionId = await context.sessions.create(clientCanElicit(params));
845
+ const elicitation = clientElicitation(params);
846
+ const sessionId = await context.sessions.create(elicitation.form || elicitation.url, elicitation.url);
759
847
  response.headers.set("Mcp-Session-Id", sessionId);
760
848
  return response;
761
849
  };
762
- var toolsList = async (config, caller, scopes, id, params) => {
850
+ var toolsList = async (config, caller, scopes, id, params, protocolVersion) => {
763
851
  const tools = await config.tools({ caller, meta: {} });
764
852
  const visible = Object.entries(tools).filter(([, tool]) => scopeAllows(tool, scopes) && agencyAllows(config, tool, scopes)).map(([name, tool]) => ({
765
853
  annotations: tool.annotations,
854
+ ...tool.coaz === undefined ? {} : { coaz: tool.coaz },
766
855
  description: tool.description,
767
856
  inputSchema: tool.inputSchema,
768
857
  name,
769
- ...tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }
858
+ ...tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema },
859
+ ...protocolVersion === MCP_LATEST_PROTOCOL_VERSION && tool.taskSupport !== undefined ? { execution: { taskSupport: tool.taskSupport } } : {}
770
860
  }));
771
861
  const { items, nextCursor } = paginate(visible, decodeCursor(params), config.listPageSize ?? DEFAULT_LIST_PAGE_SIZE);
772
862
  return rpcResult(id, {
@@ -777,6 +867,7 @@ var toolsList = async (config, caller, scopes, id, params) => {
777
867
  var errorResult = (id, text) => rpcResult(id, { content: [{ text, type: "text" }], isError: true });
778
868
  var noElicit = {
779
869
  canElicit: false,
870
+ canElicitUrl: false,
780
871
  elicit: () => Promise.resolve({ action: "unsupported" })
781
872
  };
782
873
  var SSE_HEADERS = {
@@ -869,7 +960,7 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
869
960
  await config.onCall({ args, caller, meta, name, ok });
870
961
  return payload;
871
962
  };
872
- var toolsCallStreaming = (config, caller, scopes, id, name, args, meta, tool, sessions, canElicit) => {
963
+ var toolsCallStreaming = (config, caller, scopes, id, name, args, meta, tool, sessions, canElicit, canElicitUrl) => {
873
964
  const encoder = new TextEncoder;
874
965
  const body = new ReadableStream({
875
966
  async start(controller) {
@@ -885,9 +976,24 @@ var toolsCallStreaming = (config, caller, scopes, id, name, args, meta, tool, se
885
976
  };
886
977
  const context = {
887
978
  canElicit,
979
+ canElicitUrl,
888
980
  elicit: async (request) => {
889
981
  if (!canElicit)
890
982
  return { action: "unsupported" };
983
+ if (request.mode === "url") {
984
+ if (!canElicitUrl)
985
+ return { action: "unsupported" };
986
+ let url;
987
+ try {
988
+ url = new URL(request.url);
989
+ } catch {
990
+ throw new Error("URL elicitation requires a valid URL");
991
+ }
992
+ const localDevelopment = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
993
+ if (url.protocol !== "https:" && !localDevelopment || url.username !== "" || url.password !== "") {
994
+ throw new Error("URL elicitation requires HTTPS without embedded credentials");
995
+ }
996
+ }
891
997
  const pending = sessions.startElicit(request);
892
998
  send({
893
999
  id: pending.id,
@@ -929,8 +1035,22 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
929
1035
  return rpcError(id, JSONRPC_INVALID_PARAMS, `Unknown tool: ${name}`);
930
1036
  }
931
1037
  const tasks = config.tasks;
932
- if (tasks !== undefined && await tasks.shouldCreate({ args, caller, name })) {
933
- if (!supportsTasks(params)) {
1038
+ const nativeTasks = context.protocolVersion === MCP_LATEST_PROTOCOL_VERSION;
1039
+ const requestedTaskParams = isRecord(params.task) ? params.task : undefined;
1040
+ const requestedTask = requestedTaskParams !== undefined;
1041
+ const taskSupport = tool.taskSupport ?? "forbidden";
1042
+ if (nativeTasks && requestedTask && tasks === undefined) {
1043
+ return rpcError(id, JSONRPC_METHOD_NOT_FOUND, "Tasks are not configured");
1044
+ }
1045
+ if (nativeTasks && requestedTask && taskSupport === "forbidden") {
1046
+ return rpcError(id, JSONRPC_METHOD_NOT_FOUND, "Tool does not support task execution");
1047
+ }
1048
+ if (nativeTasks && !requestedTask && taskSupport === "required") {
1049
+ return rpcError(id, JSONRPC_METHOD_NOT_FOUND, "Tool requires task execution");
1050
+ }
1051
+ const shouldCreateTask = tasks !== undefined && (nativeTasks ? requestedTask && taskSupport !== "forbidden" : await tasks.shouldCreate({ args, caller, name }));
1052
+ if (tasks !== undefined && shouldCreateTask) {
1053
+ if (!nativeTasks && !supportsTasks(params)) {
934
1054
  return rpcError(id, JSONRPC_MISSING_REQUIRED_CLIENT_CAPABILITY, "Missing required client capability", {
935
1055
  requiredCapabilities: {
936
1056
  extensions: { "io.modelcontextprotocol/tasks": {} }
@@ -938,6 +1058,7 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
938
1058
  });
939
1059
  }
940
1060
  const createdAt = new Date().toISOString();
1061
+ const requestedTtl = requestedTaskParams !== undefined && typeof requestedTaskParams.ttl === "number" && requestedTaskParams.ttl >= 0 ? requestedTaskParams.ttl : undefined;
941
1062
  const task = {
942
1063
  authorizationKey: await tasks.authorizationKey(caller),
943
1064
  createdAt,
@@ -945,7 +1066,7 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
945
1066
  pollIntervalMs: tasks.pollIntervalMs,
946
1067
  status: "working",
947
1068
  taskId: crypto.randomUUID(),
948
- ttlMs: tasks.ttlMs ?? null
1069
+ ttlMs: tasks.ttlMs ?? requestedTtl ?? null
949
1070
  };
950
1071
  await tasks.store.save(task);
951
1072
  setTimeout(() => {
@@ -953,7 +1074,7 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
953
1074
  const result = isRecord(payload2) && isRecord(payload2.result) ? payload2.result : { content: [], isError: true };
954
1075
  await tasks.store.update(task.taskId, {
955
1076
  result,
956
- status: "completed"
1077
+ status: result.isError === true ? "failed" : "completed"
957
1078
  });
958
1079
  }).catch(async (error) => {
959
1080
  await tasks.store.update(task.taskId, {
@@ -965,12 +1086,12 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
965
1086
  });
966
1087
  });
967
1088
  }, 0);
968
- return rpcResult(id, { ...publicMcpTask(task), resultType: "task" });
1089
+ return nativeTasks ? rpcResult(id, { task: publicMcpTask(task) }) : rpcResult(id, { ...publicMcpTask(task), resultType: "task" });
969
1090
  }
970
1091
  const sessions = context.sessions;
971
1092
  const session = sessions ? await sessions.get(context.sessionId ?? null) : null;
972
1093
  if (tool.mayElicit === true && config.elicitation?.enabled === true && sessions && session) {
973
- return toolsCallStreaming(config, caller, scopes, id, name, args, meta, tool, sessions, session.canElicit);
1094
+ return toolsCallStreaming(config, caller, scopes, id, name, args, meta, tool, sessions, session.canElicit, session.canElicitUrl ?? false);
974
1095
  }
975
1096
  const payload = await runTool(config, caller, scopes, id, name, args, meta, tool, noElicit);
976
1097
  return new Response(JSON.stringify(payload), {
@@ -995,11 +1116,70 @@ var authorizedTask = async (config, caller, params) => {
995
1116
  const authorizationKey = await config.tasks.authorizationKey(caller);
996
1117
  return task.authorizationKey === authorizationKey ? task : null;
997
1118
  };
998
- var tasksGet = async (config, caller, id, params) => {
1119
+ var tasksGet = async (config, caller, id, params, native) => {
999
1120
  const task = await authorizedTask(config, caller, params);
1000
1121
  if (task === null)
1001
1122
  return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
1002
- return rpcResult(id, { ...publicMcpTask(task), resultType: "complete" });
1123
+ return rpcResult(id, native ? publicMcpTask(task) : { ...publicMcpTask(task), resultType: "complete" });
1124
+ };
1125
+ var tasksResult = async (config, caller, id, params, signal) => {
1126
+ let task = await authorizedTask(config, caller, params);
1127
+ if (task === null)
1128
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
1129
+ while (task.status === "working" || task.status === "input_required") {
1130
+ if (signal?.aborted) {
1131
+ return rpcError(id, JSONRPC_INTERNAL_ERROR, "Task result request cancelled");
1132
+ }
1133
+ await new Promise((resolve) => setTimeout(resolve, task?.pollIntervalMs ?? 100));
1134
+ task = await authorizedTask(config, caller, params);
1135
+ if (task === null)
1136
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
1137
+ }
1138
+ if (task.error !== undefined) {
1139
+ return rpcError(id, typeof task.error.code === "number" ? task.error.code : JSONRPC_INTERNAL_ERROR, typeof task.error.message === "string" ? task.error.message : "Task failed");
1140
+ }
1141
+ const result = task.result ?? {
1142
+ content: [],
1143
+ isError: task.status !== "completed"
1144
+ };
1145
+ return rpcResult(id, {
1146
+ ...result,
1147
+ _meta: {
1148
+ ...isRecord(result._meta) ? result._meta : {},
1149
+ "io.modelcontextprotocol/related-task": { taskId: task.taskId }
1150
+ }
1151
+ });
1152
+ };
1153
+ var tasksList = async (config, caller, id, params) => {
1154
+ if (config.tasks === undefined) {
1155
+ return rpcError(id, JSONRPC_METHOD_NOT_FOUND, "Tasks are not configured");
1156
+ }
1157
+ const authorizationKey = await config.tasks.authorizationKey(caller);
1158
+ let offset = 0;
1159
+ if (isRecord(params) && params.cursor !== undefined) {
1160
+ if (typeof params.cursor !== "string") {
1161
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Invalid task cursor");
1162
+ }
1163
+ try {
1164
+ offset = Number.parseInt(atob(params.cursor), 10);
1165
+ } catch {
1166
+ offset = -1;
1167
+ }
1168
+ if (!Number.isSafeInteger(offset) || offset < 0) {
1169
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Invalid task cursor");
1170
+ }
1171
+ }
1172
+ const pageSize = Math.min(100, Math.max(1, config.tasks.listPageSize ?? 50));
1173
+ const fetched = await config.tasks.store.list(authorizationKey, {
1174
+ limit: pageSize + 1,
1175
+ offset
1176
+ });
1177
+ const items = fetched.slice(0, pageSize);
1178
+ const nextCursor = fetched.length > pageSize ? encodeCursor(offset + pageSize) : undefined;
1179
+ return rpcResult(id, {
1180
+ tasks: items.map(publicMcpTask),
1181
+ ...nextCursor === undefined ? {} : { nextCursor }
1182
+ });
1003
1183
  };
1004
1184
  var tasksUpdate = async (config, caller, id, params) => {
1005
1185
  const task = await authorizedTask(config, caller, params);
@@ -1013,12 +1193,16 @@ var tasksUpdate = async (config, caller, id, params) => {
1013
1193
  });
1014
1194
  return rpcResult(id, { resultType: "complete" });
1015
1195
  };
1016
- var tasksCancel = async (config, caller, id, params) => {
1196
+ var tasksCancel = async (config, caller, id, params, native) => {
1017
1197
  const task = await authorizedTask(config, caller, params);
1018
1198
  if (task === null)
1019
1199
  return rpcError(id, JSONRPC_INVALID_PARAMS, "Unknown task");
1200
+ if (["cancelled", "completed", "failed"].includes(task.status)) {
1201
+ return rpcError(id, JSONRPC_INVALID_PARAMS, "Task is already terminal");
1202
+ }
1020
1203
  await config.tasks?.store.cancel(task.taskId);
1021
- return rpcResult(id, { resultType: "complete" });
1204
+ const cancelled = await config.tasks?.store.get(task.taskId);
1205
+ return rpcResult(id, native && cancelled !== null && cancelled !== undefined ? publicMcpTask(cancelled) : { resultType: "complete" });
1022
1206
  };
1023
1207
  var promptsList = (config, id, params) => {
1024
1208
  const definitions = config.prompts?.definitions ?? {};
@@ -1089,7 +1273,10 @@ var elicitAnswer = async (message, context) => {
1089
1273
  return notificationAck();
1090
1274
  const result = isRecord(message.result) ? message.result : null;
1091
1275
  const action = result?.action;
1092
- const answer = action === "accept" && isRecord(result?.content) ? { action: "accept", content: result.content } : action === "decline" ? { action: "decline" } : { action: "cancel" };
1276
+ const answer = action === "accept" ? {
1277
+ action: "accept",
1278
+ content: isRecord(result?.content) ? result.content : {}
1279
+ } : action === "decline" ? { action: "decline" } : { action: "cancel" };
1093
1280
  await context.sessions.resolveElicit({
1094
1281
  requestId,
1095
1282
  result: answer,
@@ -1106,6 +1293,7 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
1106
1293
  if (!("method" in message))
1107
1294
  return elicitAnswer(message, context);
1108
1295
  const id = idOf(message);
1296
+ const protocolVersion = context.protocolVersion ?? "2025-06-18";
1109
1297
  const method = typeof message.method === "string" ? message.method : "";
1110
1298
  const { params } = message;
1111
1299
  if (method === "initialize") {
@@ -1122,17 +1310,24 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
1122
1310
  if (method === "ping")
1123
1311
  return rpcResult(id, {});
1124
1312
  if (method === "tools/list") {
1125
- return toolsList(config, caller, scopes, id, params);
1313
+ return toolsList(config, caller, scopes, id, params, protocolVersion);
1126
1314
  }
1127
1315
  if (method === "tools/call") {
1128
- return toolsCall(config, caller, scopes, id, params, context);
1316
+ return toolsCall(config, caller, scopes, id, params, {
1317
+ ...context,
1318
+ protocolVersion
1319
+ });
1129
1320
  }
1130
1321
  if (method === "tasks/get")
1131
- return tasksGet(config, caller, id, params);
1132
- if (method === "tasks/update")
1322
+ return tasksGet(config, caller, id, params, protocolVersion === MCP_LATEST_PROTOCOL_VERSION);
1323
+ if (method === "tasks/result" && protocolVersion === MCP_LATEST_PROTOCOL_VERSION)
1324
+ return tasksResult(config, caller, id, params, context.requestSignal);
1325
+ if (method === "tasks/list" && protocolVersion === MCP_LATEST_PROTOCOL_VERSION)
1326
+ return tasksList(config, caller, id, params);
1327
+ if (method === "tasks/update" && protocolVersion !== MCP_LATEST_PROTOCOL_VERSION)
1133
1328
  return tasksUpdate(config, caller, id, params);
1134
1329
  if (method === "tasks/cancel")
1135
- return tasksCancel(config, caller, id, params);
1330
+ return tasksCancel(config, caller, id, params, protocolVersion === MCP_LATEST_PROTOCOL_VERSION);
1136
1331
  if (method === "prompts/list")
1137
1332
  return promptsList(config, id, params);
1138
1333
  if (method === "prompts/get")
@@ -1261,7 +1456,11 @@ var createMemoryStore = (ttlMs) => {
1261
1456
  create: (session) => {
1262
1457
  sweep();
1263
1458
  const id = crypto.randomUUID();
1264
- sessions.set(id, { canElicit: session.canElicit, lastSeen: Date.now() });
1459
+ sessions.set(id, {
1460
+ canElicit: session.canElicit,
1461
+ canElicitUrl: session.canElicitUrl ?? false,
1462
+ lastSeen: Date.now()
1463
+ });
1265
1464
  return id;
1266
1465
  },
1267
1466
  drop: (id) => {
@@ -1272,7 +1471,10 @@ var createMemoryStore = (ttlMs) => {
1272
1471
  if (!session)
1273
1472
  return null;
1274
1473
  session.lastSeen = Date.now();
1275
- return { canElicit: session.canElicit };
1474
+ return {
1475
+ canElicit: session.canElicit,
1476
+ canElicitUrl: session.canElicitUrl
1477
+ };
1276
1478
  }
1277
1479
  };
1278
1480
  };
@@ -1307,9 +1509,9 @@ var createSessionRegistry = (options) => {
1307
1509
  });
1308
1510
  pending.clear();
1309
1511
  },
1310
- create: async (canElicit) => {
1512
+ create: async (canElicit, canElicitUrl = false) => {
1311
1513
  await ready;
1312
- return await store.create({ canElicit });
1514
+ return await store.create({ canElicit, canElicitUrl });
1313
1515
  },
1314
1516
  drop: async (id) => {
1315
1517
  await ready;
@@ -1388,12 +1590,27 @@ var runMcpPost = async (config, request, body) => {
1388
1590
  if (Array.isArray(body)) {
1389
1591
  return rpcError(null, JSONRPC_INVALID_REQUEST, "Batching is not supported");
1390
1592
  }
1593
+ const isInitialize = typeof body === "object" && body !== null && "method" in body && body.method === "initialize";
1594
+ const protocolVersion = request.headers.get("mcp-protocol-version");
1595
+ const supportedProtocols = config.supportedProtocols ?? [
1596
+ MCP_LATEST_PROTOCOL_VERSION,
1597
+ "2025-06-18",
1598
+ "2025-03-26",
1599
+ "2024-11-05"
1600
+ ];
1601
+ if (!isInitialize && (protocolVersion === null || !supportedProtocols.includes(protocolVersion))) {
1602
+ return new Response("Missing or unsupported MCP-Protocol-Version", {
1603
+ status: 400
1604
+ });
1605
+ }
1391
1606
  const sessions = registryFor(config);
1392
1607
  const sessionId = request.headers.get("mcp-session-id");
1393
1608
  if (sessions && sessionId && !await sessions.get(sessionId)) {
1394
1609
  return new Response(null, { status: HTTP_NOT_FOUND });
1395
1610
  }
1396
1611
  return dispatchMcp(config, auth.caller, auth.scopes ?? [], body, {
1612
+ protocolVersion: protocolVersion ?? MCP_LATEST_PROTOCOL_VERSION,
1613
+ requestSignal: request.signal,
1397
1614
  sessionId,
1398
1615
  sessions
1399
1616
  }).catch(() => rpcError(null, JSONRPC_INVALID_REQUEST, "Internal error"));
@@ -1470,10 +1687,12 @@ CREATE INDEX IF NOT EXISTS tasks_expiry_idx ON ${ns}.tasks (expires_at) WHERE ex
1470
1687
  CREATE TABLE IF NOT EXISTS ${ns}.sessions (
1471
1688
  session_id text PRIMARY KEY,
1472
1689
  can_elicit boolean NOT NULL,
1690
+ can_elicit_url boolean NOT NULL DEFAULT false,
1473
1691
  created_at timestamptz NOT NULL DEFAULT now(),
1474
1692
  last_seen_at timestamptz NOT NULL DEFAULT now(),
1475
1693
  expires_at timestamptz NOT NULL
1476
1694
  );
1695
+ ALTER TABLE ${ns}.sessions ADD COLUMN IF NOT EXISTS can_elicit_url boolean NOT NULL DEFAULT false;
1477
1696
  CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON ${ns}.sessions (expires_at);`;
1478
1697
  };
1479
1698
  var createPostgresMcpTaskStore = ({
@@ -1492,6 +1711,7 @@ var createPostgresMcpTaskStore = ({
1492
1711
  ]);
1493
1712
  },
1494
1713
  get: async (taskId) => (await client.query(`SELECT data FROM ${ns}.tasks WHERE task_id = $1 AND (expires_at IS NULL OR expires_at > $2::timestamptz)`, [taskId, now().toISOString()])).rows[0]?.data ?? null,
1714
+ list: async (authorizationKey, { limit, offset }) => (await client.query(`SELECT data FROM ${ns}.tasks WHERE authorization_key = $1 AND (expires_at IS NULL OR expires_at > $2::timestamptz) ORDER BY updated_at DESC LIMIT $3 OFFSET $4`, [authorizationKey, now().toISOString(), limit, offset])).rows.map((row) => row.data),
1495
1715
  save: async (task) => {
1496
1716
  const expiresAt = task.ttlMs === null ? null : new Date(new Date(task.createdAt).getTime() + task.ttlMs).toISOString();
1497
1717
  await client.query(`INSERT INTO ${ns}.tasks (task_id, authorization_key, status, created_at, updated_at, expires_at, data) VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz, $6::timestamptz, $7::jsonb) ON CONFLICT (task_id) DO NOTHING`, [
@@ -1522,12 +1742,13 @@ var createPostgresMcpSessionStore = ({
1522
1742
  }) => {
1523
1743
  const ns = namespaceOf(namespace);
1524
1744
  return {
1525
- create: async ({ canElicit }) => {
1745
+ create: async ({ canElicit, canElicitUrl }) => {
1526
1746
  const id = crypto.randomUUID();
1527
1747
  const current = now();
1528
- await client.query(`INSERT INTO ${ns}.sessions (session_id, can_elicit, created_at, last_seen_at, expires_at) VALUES ($1, $2, $3::timestamptz, $3::timestamptz, $4::timestamptz)`, [
1748
+ await client.query(`INSERT INTO ${ns}.sessions (session_id, can_elicit, can_elicit_url, created_at, last_seen_at, expires_at) VALUES ($1, $2, $3, $4::timestamptz, $4::timestamptz, $5::timestamptz)`, [
1529
1749
  id,
1530
1750
  canElicit,
1751
+ canElicitUrl ?? false,
1531
1752
  current.toISOString(),
1532
1753
  new Date(current.getTime() + ttlMs).toISOString()
1533
1754
  ]);
@@ -1540,13 +1761,16 @@ var createPostgresMcpSessionStore = ({
1540
1761
  },
1541
1762
  get: async (id) => {
1542
1763
  const current = now();
1543
- const result = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit`, [
1764
+ const result = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit, can_elicit_url`, [
1544
1765
  id,
1545
1766
  current.toISOString(),
1546
1767
  new Date(current.getTime() + ttlMs).toISOString()
1547
1768
  ]);
1548
1769
  const row = result.rows[0];
1549
- return row === undefined ? null : { canElicit: row.can_elicit };
1770
+ return row === undefined ? null : {
1771
+ canElicit: row.can_elicit,
1772
+ canElicitUrl: row.can_elicit_url
1773
+ };
1550
1774
  }
1551
1775
  };
1552
1776
  };
@@ -1571,5 +1795,6 @@ export {
1571
1795
  createMcpClient,
1572
1796
  createMcpAuthorizationRequest,
1573
1797
  McpClientError,
1798
+ MCP_LATEST_PROTOCOL_VERSION,
1574
1799
  FEEDBACK_INSTRUCTIONS
1575
1800
  };
package/dist/manifest.js CHANGED
@@ -5916,6 +5916,14 @@ var serializedTool = Type.Object({
5916
5916
  });
5917
5917
  var manifestSchema = Type.Object({
5918
5918
  contract: Type.Union([Type.Literal(1), Type.Literal(2)]),
5919
+ discovery: Type.Optional(Type.Object({
5920
+ audiences: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5921
+ certificationUrl: Type.Optional(Type.String()),
5922
+ intents: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5923
+ keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5924
+ protocols: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5925
+ url: Type.Optional(Type.String())
5926
+ })),
5919
5927
  identity: Type.Object({
5920
5928
  accent: Type.Optional(Type.String({ pattern: "^#[0-9a-fA-F]{3,8}$" })),
5921
5929
  category: Type.String({ minLength: 1 }),
@@ -34,10 +34,21 @@ export type McpClientOptions = {
34
34
  };
35
35
  export type McpRemoteTool = {
36
36
  annotations?: McpToolAnnotations;
37
+ coaz?: boolean;
37
38
  description?: string;
38
39
  inputSchema?: Record<string, unknown>;
39
40
  name: string;
40
41
  outputSchema?: Record<string, unknown>;
42
+ taskSupport?: "forbidden" | "optional" | "required";
43
+ };
44
+ export type McpRemoteTask = {
45
+ createdAt: string;
46
+ lastUpdatedAt: string;
47
+ pollInterval?: number;
48
+ status: "working" | "input_required" | "completed" | "failed" | "cancelled";
49
+ statusMessage?: string;
50
+ taskId: string;
51
+ ttl: number | null;
41
52
  };
42
53
  export type McpInitializeResult = {
43
54
  capabilities?: Record<string, unknown>;
@@ -51,7 +62,14 @@ export type McpInitializeResult = {
51
62
  };
52
63
  export type McpClient = {
53
64
  callTool: (name: string, args?: unknown) => Promise<McpToolResult>;
65
+ callToolAsTask: (name: string, args?: unknown, options?: {
66
+ ttl?: number;
67
+ }) => Promise<McpRemoteTask>;
68
+ cancelTask: (taskId: string) => Promise<McpRemoteTask>;
69
+ getTask: (taskId: string) => Promise<McpRemoteTask>;
70
+ getTaskResult: (taskId: string) => Promise<McpToolResult>;
54
71
  initialize: () => Promise<McpInitializeResult>;
72
+ listTasks: () => Promise<McpRemoteTask[]>;
55
73
  listResources: () => Promise<unknown[]>;
56
74
  listTools: () => Promise<McpRemoteTool[]>;
57
75
  ping: () => Promise<void>;
@@ -4,7 +4,10 @@ import type { McpServerConfig } from "./types";
4
4
  * Only elicitation uses it; without it the dispatcher is exactly as stateless
5
5
  * as it was. */
6
6
  export type McpDispatchContext = {
7
+ protocolVersion?: string;
8
+ requestSignal?: AbortSignal;
7
9
  sessionId?: string | null;
8
10
  sessions?: SessionRegistry;
9
11
  };
12
+ export declare const MCP_LATEST_PROTOCOL_VERSION: "2025-11-25";
10
13
  export declare const dispatchMcp: <Caller>(config: McpServerConfig<Caller>, caller: Caller, scopes: string[], message: unknown, context?: McpDispatchContext) => Promise<Response>;
@@ -31,9 +31,9 @@
31
31
  * depends on a model.
32
32
  */
33
33
  export { verifyBearer, type BearerResult, type BearerVerifier, type VerifiedJwt, type VerifyBearerConfig, } from "./auth";
34
- export { createMcpClient, McpClientError, type McpClient, type McpClientOptions, type McpInitializeResult, type McpRemoteTool, } from "./client";
34
+ export { createMcpClient, McpClientError, type McpClient, type McpClientOptions, type McpInitializeResult, type McpRemoteTask, type McpRemoteTool, } from "./client";
35
35
  export * from "./oauth";
36
- export { dispatchMcp, type McpDispatchContext } from "./dispatch";
36
+ export { dispatchMcp, MCP_LATEST_PROTOCOL_VERSION, type McpDispatchContext, } from "./dispatch";
37
37
  export { FEEDBACK_INSTRUCTIONS, feedbackTools, type McpFeedbackRating, type McpFeedbackReport, type McpFeedbackStore, type McpProblemReport, } from "./feedback";
38
38
  export { createMcpHandler } from "./handler";
39
39
  export { metadataPathFor, protectedResourceMetadata, type ProtectedResourceMetadata, } from "./metadata";
@@ -41,4 +41,4 @@ export { mcpServer } from "./server";
41
41
  export { createSessionRegistry, type SessionRegistry } from "./sessions";
42
42
  export { createMemoryMcpTaskStore, publicMcpTask } from "./tasks";
43
43
  export { createPostgresMcpSessionStore, createPostgresMcpTaskStore, mcpPostgresSchemaSql, type McpSqlClient, type McpSqlResult, } from "./postgres";
44
- 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";
44
+ export type { McpAgencyOptions, McpAudioContent, McpElicitAnswer, McpElicitationRequest, McpElicitBus, McpElicitResult, McpFormElicitationRequest, 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, McpUrlElicitationRequest, } from "./types";
@@ -8,10 +8,11 @@ export declare const createSessionRegistry: (options?: {
8
8
  }) => {
9
9
  ready: Promise<void>;
10
10
  close: () => Promise<void>;
11
- create: (canElicit: boolean) => Promise<string>;
11
+ create: (canElicit: boolean, canElicitUrl?: boolean) => Promise<string>;
12
12
  drop: (id: string) => Promise<void>;
13
13
  get: (id: string | null) => Promise<{
14
14
  canElicit: boolean;
15
+ canElicitUrl?: boolean;
15
16
  } | null>;
16
17
  /** The client answered. If the call that asked is running HERE, resolve it.
17
18
  * If not, put the answer on the bus so the instance that is waiting can —
@@ -1,14 +1,11 @@
1
1
  import type { McpTask, McpTaskStore } from "./types";
2
2
  export declare const createMemoryMcpTaskStore: () => McpTaskStore;
3
3
  export declare const publicMcpTask: ({ authorizationKey, ...task }: McpTask) => {
4
+ ttl: number | null;
5
+ pollInterval?: number | undefined;
4
6
  createdAt: string;
5
- error?: Record<string, unknown>;
6
- inputRequests?: Record<string, unknown>;
7
7
  lastUpdatedAt: string;
8
- pollIntervalMs?: number;
9
- result?: Record<string, unknown>;
10
8
  status: import("./types").McpTaskStatus;
11
9
  statusMessage?: string;
12
10
  taskId: string;
13
- ttlMs: number | null;
14
11
  };
@@ -49,10 +49,18 @@ export type McpToolReturn = McpContent[] | McpToolResult | string;
49
49
  * arrays are not allowed by the spec.
50
50
  *
51
51
  * Servers MUST NOT elicit sensitive information (spec, Security). */
52
- export type McpElicitationRequest = {
52
+ export type McpFormElicitationRequest = {
53
53
  message: string;
54
+ mode?: "form";
54
55
  requestedSchema: Record<string, unknown>;
55
56
  };
57
+ export type McpUrlElicitationRequest = {
58
+ elicitationId: string;
59
+ message: string;
60
+ mode: "url";
61
+ url: string;
62
+ };
63
+ export type McpElicitationRequest = McpFormElicitationRequest | McpUrlElicitationRequest;
56
64
  /** What came back. `unsupported` is ours, not the spec's: it is what you get
57
65
  * when the client never declared the elicitation capability, so a tool can
58
66
  * fall back instead of pretending the user declined. */
@@ -78,12 +86,15 @@ export type McpElicitAnswer = {
78
86
  export type McpSessionStore = {
79
87
  create: (session: {
80
88
  canElicit: boolean;
89
+ canElicitUrl?: boolean;
81
90
  }) => Promise<string> | string;
82
91
  drop: (id: string) => Promise<void> | void;
83
92
  get: (id: string) => Promise<{
84
93
  canElicit: boolean;
94
+ canElicitUrl?: boolean;
85
95
  } | null> | {
86
96
  canElicit: boolean;
97
+ canElicitUrl?: boolean;
87
98
  } | null;
88
99
  };
89
100
  /** How an answer reaches the instance that asked the question. The tool call
@@ -102,6 +113,8 @@ export type McpToolCallContext = {
102
113
  /** True when this client can actually show the user a form. Check it before
103
114
  * designing a flow around elicit(). */
104
115
  canElicit: boolean;
116
+ /** True only when the client negotiated secure URL-mode elicitation. */
117
+ canElicitUrl: boolean;
105
118
  /** Ask the user a question and wait for the answer. Resolves to
106
119
  * `{action:"unsupported"}` immediately when the client can't elicit, and to
107
120
  * `{action:"cancel"}` if they never answer. */
@@ -110,6 +123,9 @@ export type McpToolCallContext = {
110
123
  /** One callable tool. `inputSchema` is a JSON Schema object. */
111
124
  export type McpTool = {
112
125
  annotations?: McpToolAnnotations;
126
+ /** OpenID AuthZEN COAZ opt-in marker. When true, inputSchema MUST carry an
127
+ * `x-coaz-mapping`; hosts should evaluate it before invoking the handler. */
128
+ coaz?: boolean;
113
129
  /** Enforceable semantic effects from manifest contract 2. A tool carrying
114
130
  * this is hidden unless the server configures `agency`. */
115
131
  authorization?: ToolAuthorization;
@@ -123,6 +139,9 @@ export type McpTool = {
123
139
  mayElicit?: boolean;
124
140
  /** JSON Schema for `structuredContent`, advertised on `tools/list`. */
125
141
  outputSchema?: Record<string, unknown>;
142
+ /** MCP 2025-11-25 task augmentation support for this tool. Omitted means
143
+ * forbidden, as required by the specification. */
144
+ taskSupport?: "forbidden" | "optional" | "required";
126
145
  /** If set, the tool is only listed and callable when the caller's scopes
127
146
  * include this. Tools without a scope are always available. Fails closed:
128
147
  * a scoped tool is hidden when the caller's scopes are unknown. */
@@ -195,6 +214,11 @@ export type McpTask = {
195
214
  export type McpTaskStore = {
196
215
  cancel: (taskId: string) => Promise<void> | void;
197
216
  get: (taskId: string) => Promise<McpTask | null> | McpTask | null;
217
+ /** List only tasks owned by this authorization context, newest first. */
218
+ list: (authorizationKey: string, options: {
219
+ limit: number;
220
+ offset: number;
221
+ }) => Promise<McpTask[]> | McpTask[];
198
222
  save: (task: McpTask) => Promise<void> | void;
199
223
  update: (taskId: string, update: Partial<Omit<McpTask, "authorizationKey" | "createdAt" | "taskId">>) => Promise<McpTask | null> | McpTask | null;
200
224
  };
@@ -206,6 +230,8 @@ export type McpTasksOptions<Caller> = {
206
230
  task: McpTask;
207
231
  }) => Promise<void> | void;
208
232
  pollIntervalMs?: number;
233
+ /** Maximum tasks returned by one tasks/list page (default 50, max 100). */
234
+ listPageSize?: number;
209
235
  shouldCreate: (context: {
210
236
  args: unknown;
211
237
  caller: Caller;
@@ -295,7 +321,8 @@ export type McpServerConfig<Caller> = {
295
321
  /** Protocol versions this endpoint accepts; the first is the preferred one.
296
322
  * Defaults to the versions this package knows. */
297
323
  supportedProtocols?: string[];
298
- /** Final SEP-2663 `io.modelcontextprotocol/tasks` extension support. */
324
+ /** MCP 2025-11-25 native tasks plus legacy SEP-2663 compatibility for
325
+ * older negotiated protocol versions. */
299
326
  tasks?: McpTasksOptions<Caller>;
300
327
  /** Build the tool registry for this caller. Called once per request. */
301
328
  tools: (ctx: McpToolContext<Caller>) => McpToolRegistry | Promise<McpToolRegistry>;
package/package.json CHANGED
@@ -11,8 +11,8 @@
11
11
  "elysia": ">=1.1.0"
12
12
  },
13
13
  "dependencies": {
14
- "@absolutejs/agency": "^0.3.0",
15
- "@absolutejs/manifest": "^0.2.0",
14
+ "@absolutejs/agency": "^0.4.0",
15
+ "@absolutejs/manifest": "^0.3.0",
16
16
  "@sinclair/typebox": "^0.34.0"
17
17
  },
18
18
  "license": "BUSL-1.1",
@@ -58,5 +58,5 @@
58
58
  "typecheck": "tsc --noEmit --project tsconfig.json"
59
59
  },
60
60
  "types": "./dist/src/index.d.ts",
61
- "version": "0.8.0"
61
+ "version": "0.10.0"
62
62
  }