@wakeshift/sdk 0.4.0 → 0.5.2

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/dist/tasks.js ADDED
@@ -0,0 +1,147 @@
1
+ // MCP Tasks as a wake path (docs/harnesses.md, "Claude Code: MCP tasks as a
2
+ // wake path"): for a client that takes MCP tasks, on protocol 2026-07-28
3
+ // (discover.ts), attach serves Wakeshift's wait tool, wakeshift_wait. Its
4
+ // call is answered with a task that completes when the conversation's next
5
+ // Wakeshift messages arrive; the client follows it in the background
6
+ // (tasks/get), and a completed task wakes an idle conversation. Wakeshift
7
+ // decides everything: the SDK passes the tool's call, tasks/get and
8
+ // tasks/cancel to it and answers with the MCP result it returns, so every
9
+ // SDK says the same (test/mcp-tasks-wire.json). A task is only its ID,
10
+ // which any process answers for, so nothing is kept here.
11
+ //
12
+ // Only a request that declares the tasks extension in its own capabilities
13
+ // (2026-07-28 has no initialize: each request carries them) sees the wait
14
+ // tool, and a wrapped tool's call from it tells Wakeshift so (call.tasks),
15
+ // which then gives the wait tool's instructions instead of its harness's
16
+ // zero-install path (Claude Code's Monitor). Everything else is unchanged.
17
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
18
+ import { WakeshiftError } from "./errors.js";
19
+ import { TASKS_EXTENSION, takesTasks } from "./discover.js";
20
+ import { WAKE_ARG_DESCRIPTIONS } from "./toolcall.js";
21
+ import { toolCallOf } from "./wakeshift.js";
22
+ /** The wait tool's name. */
23
+ export const WAIT_TOOL = "wakeshift_wait";
24
+ /** The wait tool's description: the core's harnesses.WaitToolDescription. */
25
+ export const WAIT_TOOL_DESCRIPTION = "Wait for this conversation's next Wakeshift messages from this app. Returns at once and keeps waiting in the background: when messages arrive, its result reaches you as a task notification. Call it again after each one, with the session_key Wakeshift gave you.";
26
+ /** What every Wakeshift task ID starts with: tasks/get and tasks/cancel for any other ID aren't Wakeshift's. */
27
+ export const TASK_ID_PREFIX = "wkt1_";
28
+ /** How long Wakeshift holds a tasks/get for a working task, in seconds. */
29
+ export const TASK_HOLD_SECONDS = 20;
30
+ /** The wait tool as tools/list lists it. */
31
+ export const WAIT_TOOL_DEFINITION = {
32
+ name: WAIT_TOOL,
33
+ description: WAIT_TOOL_DESCRIPTION,
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ session_key: { type: "string", description: WAKE_ARG_DESCRIPTIONS.session_key },
38
+ harness: { type: "string", description: WAKE_ARG_DESCRIPTIONS.harness },
39
+ conversation_id: { type: "string", description: WAKE_ARG_DESCRIPTIONS.conversation_id },
40
+ },
41
+ additionalProperties: false,
42
+ },
43
+ };
44
+ export { TASKS_EXTENSION, takesTasks };
45
+ /**
46
+ * @internal Serves the wait tool, tasks/get and tasks/cancel on server
47
+ * (after serveStateless): the tasks extension in server/discover, the tool
48
+ * in tools/list for a request that takes tasks, and its calls and task
49
+ * requests passed to Wakeshift.
50
+ */
51
+ export function serveTasks(wake, server, callerOf) {
52
+ const low = server.server;
53
+ const handlers = low._requestHandlers;
54
+ if (!(handlers instanceof Map))
55
+ return; // a future SDK: nothing to hook (no wait tool)
56
+ const call = (extra, args) => {
57
+ const { user, client } = callerOf(extra);
58
+ return toolCallOf(extra, client, user, args, WAIT_TOOL);
59
+ };
60
+ const waitCall = async (request, extra) => {
61
+ const args = (request?.params?.arguments ?? {});
62
+ try {
63
+ const out = await wake.requestApp("POST", "/calls/task", { call: call(extra, args) });
64
+ if (out.result)
65
+ return out.result;
66
+ }
67
+ catch (err) {
68
+ return { isError: true, content: [{ type: "text", text: `Wakeshift couldn't start the wait (${detailOf(err)}). Call ${WAIT_TOOL} again.` }] };
69
+ }
70
+ return { isError: true, content: [{ type: "text", text: `Wakeshift didn't answer the wait. Call ${WAIT_TOOL} again.` }] };
71
+ };
72
+ const taskRequest = (path, hold) => async (request, extra) => {
73
+ const task = request?.params?.taskId;
74
+ const body = { task: typeof task === "string" ? task : "", call: call(extra, {}) };
75
+ if (hold)
76
+ body.hold_seconds = TASK_HOLD_SECONDS;
77
+ let out;
78
+ try {
79
+ out = await wake.requestApp("POST", path, body, { timeoutMs: (TASK_HOLD_SECONDS + 15) * 1000, signal: extra?.signal });
80
+ }
81
+ catch (err) {
82
+ if (extra?.signal?.aborted)
83
+ throw new McpError(-32603, "canceled by the client");
84
+ throw new McpError(-32603, `Wakeshift: ${detailOf(err)}`);
85
+ }
86
+ if (out.rpc_error)
87
+ throw new McpError(out.rpc_error.code, out.rpc_error.message);
88
+ if (!out.result)
89
+ throw new McpError(-32603, "Wakeshift didn't answer for this task");
90
+ return out.result;
91
+ };
92
+ // server/discover (discover.ts) gets the tasks extension.
93
+ const wrapDiscover = (h) => async (request, extra) => {
94
+ const out = await h(request, extra);
95
+ if (out && typeof out === "object") {
96
+ const caps = (out.capabilities ??= {});
97
+ caps.extensions = { ...(caps.extensions ?? {}), [TASKS_EXTENSION]: {} };
98
+ }
99
+ return out;
100
+ };
101
+ // tools/list lists the wait tool for a request that takes tasks; tools/call
102
+ // answers it. McpServer sets these when the first tool is registered,
103
+ // perhaps after attach, so a later set is wrapped too.
104
+ const wrapList = (h) => async (request, extra) => {
105
+ const out = await h(request, extra);
106
+ if (takesTasks(request?.params?._meta, extra) && out && Array.isArray(out.tools) && !out.tools.some((t) => t?.name === WAIT_TOOL)) {
107
+ out.tools = [...out.tools, WAIT_TOOL_DEFINITION];
108
+ }
109
+ return out;
110
+ };
111
+ const wrapCall = (h) => async (request, extra) => (request?.params?.name === WAIT_TOOL ? waitCall(request, extra) : h(request, extra));
112
+ // tasks/get and tasks/cancel: Wakeshift's own tasks (their IDs start
113
+ // wkt1_) go to Wakeshift; any other goes to the app's own handler, if it
114
+ // has one.
115
+ const wrapTask = (path, hold) => (h) => {
116
+ const ours = taskRequest(path, hold);
117
+ return async (request, extra) => {
118
+ const id = request?.params?.taskId;
119
+ if (typeof id === "string" && id.startsWith(TASK_ID_PREFIX))
120
+ return ours(request, extra);
121
+ if (h)
122
+ return h(request, extra);
123
+ throw new McpError(-32601, "Method not found");
124
+ };
125
+ };
126
+ const wrappers = {
127
+ "server/discover": wrapDiscover,
128
+ "tools/list": wrapList,
129
+ "tools/call": wrapCall,
130
+ "tasks/get": wrapTask("/calls/task/get", true),
131
+ "tasks/cancel": wrapTask("/calls/task/cancel", false),
132
+ };
133
+ // Set directly, not through the MCP SDK's setRequestHandler, which would
134
+ // ask for its own tasks capability (an older revision's), never advertised;
135
+ // and once only (serveStateless wraps what's set through handlers.set).
136
+ for (const [method, wrap] of Object.entries(wrappers)) {
137
+ const h = handlers.get(method);
138
+ if (h || method.startsWith("tasks/"))
139
+ Map.prototype.set.call(handlers, method, wrap(h));
140
+ }
141
+ const set = handlers.set.bind(handlers);
142
+ handlers.set = (method, h) => set(method, wrappers[method] ? wrappers[method](h) : h);
143
+ }
144
+ function detailOf(err) {
145
+ return err instanceof WakeshiftError ? err.detail : String(err?.message ?? err);
146
+ }
147
+ //# sourceMappingURL=tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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","sourcesContent":["// MCP Tasks as a wake path (docs/harnesses.md, \"Claude Code: MCP tasks as a\n// wake path\"): for a client that takes MCP tasks, on protocol 2026-07-28\n// (discover.ts), attach serves Wakeshift's wait tool, wakeshift_wait. Its\n// call is answered with a task that completes when the conversation's next\n// Wakeshift messages arrive; the client follows it in the background\n// (tasks/get), and a completed task wakes an idle conversation. Wakeshift\n// decides everything: the SDK passes the tool's call, tasks/get and\n// tasks/cancel to it and answers with the MCP result it returns, so every\n// SDK says the same (test/mcp-tasks-wire.json). A task is only its ID,\n// which any process answers for, so nothing is kept here.\n//\n// Only a request that declares the tasks extension in its own capabilities\n// (2026-07-28 has no initialize: each request carries them) sees the wait\n// tool, and a wrapped tool's call from it tells Wakeshift so (call.tasks),\n// which then gives the wait tool's instructions instead of its harness's\n// zero-install path (Claude Code's Monitor). Everything else is unchanged.\n\nimport { McpError } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { WakeshiftError } from \"./errors.js\";\nimport { TASKS_EXTENSION, takesTasks } from \"./discover.js\";\nimport { WAKE_ARG_DESCRIPTIONS } from \"./toolcall.js\";\nimport { toolCallOf, type ToolExtra, type Wakeshift } from \"./wakeshift.js\";\n\n/** The wait tool's name. */\nexport const WAIT_TOOL = \"wakeshift_wait\";\n\n/** The wait tool's description: the core's harnesses.WaitToolDescription. */\nexport const WAIT_TOOL_DESCRIPTION =\n \"Wait for this conversation's next Wakeshift messages from this app. Returns at once and keeps waiting in the background: when messages arrive, its result reaches you as a task notification. Call it again after each one, with the session_key Wakeshift gave you.\";\n\n/** What every Wakeshift task ID starts with: tasks/get and tasks/cancel for any other ID aren't Wakeshift's. */\nexport const TASK_ID_PREFIX = \"wkt1_\";\n\n/** How long Wakeshift holds a tasks/get for a working task, in seconds. */\nexport const TASK_HOLD_SECONDS = 20;\n\n/** The wait tool as tools/list lists it. */\nexport const WAIT_TOOL_DEFINITION = {\n name: WAIT_TOOL,\n description: WAIT_TOOL_DESCRIPTION,\n inputSchema: {\n type: \"object\",\n properties: {\n session_key: { type: \"string\", description: WAKE_ARG_DESCRIPTIONS.session_key },\n harness: { type: \"string\", description: WAKE_ARG_DESCRIPTIONS.harness },\n conversation_id: { type: \"string\", description: WAKE_ARG_DESCRIPTIONS.conversation_id },\n },\n additionalProperties: false,\n },\n} as const;\n\nexport { TASKS_EXTENSION, takesTasks };\n\ntype Handler = (request: any, extra: any) => Promise<any>;\n\n/** What the core answers a task request with. */\ninterface TaskAnswer {\n result?: Record<string, unknown>;\n rpc_error?: { code: number; message: string };\n}\n\n/** @internal Who's calling, for a request: the user (identify's) and the client's name. */\nexport type CallerOf = (extra: ToolExtra) => { user: string | undefined; client: string };\n\n/**\n * @internal Serves the wait tool, tasks/get and tasks/cancel on server\n * (after serveStateless): the tasks extension in server/discover, the tool\n * in tools/list for a request that takes tasks, and its calls and task\n * requests passed to Wakeshift.\n */\nexport function serveTasks(wake: Wakeshift, server: McpServer, callerOf: CallerOf): void {\n const low = server.server as any;\n const handlers = low._requestHandlers as Map<string, Handler> | undefined;\n if (!(handlers instanceof Map)) return; // a future SDK: nothing to hook (no wait tool)\n\n const call = (extra: any, args: Record<string, unknown>) => {\n const { user, client } = callerOf(extra);\n return toolCallOf(extra, client, user, args, WAIT_TOOL);\n };\n const waitCall: Handler = async (request, extra) => {\n const args = (request?.params?.arguments ?? {}) as Record<string, unknown>;\n try {\n const out = await wake.requestApp<TaskAnswer>(\"POST\", \"/calls/task\", { call: call(extra, args) });\n if (out.result) return out.result;\n } catch (err) {\n return { isError: true, content: [{ type: \"text\", text: `Wakeshift couldn't start the wait (${detailOf(err)}). Call ${WAIT_TOOL} again.` }] };\n }\n return { isError: true, content: [{ type: \"text\", text: `Wakeshift didn't answer the wait. Call ${WAIT_TOOL} again.` }] };\n };\n const taskRequest = (path: string, hold: boolean): Handler => async (request, extra) => {\n const task = request?.params?.taskId;\n const body: Record<string, unknown> = { task: typeof task === \"string\" ? task : \"\", call: call(extra, {}) };\n if (hold) body.hold_seconds = TASK_HOLD_SECONDS;\n let out: TaskAnswer;\n try {\n out = await wake.requestApp<TaskAnswer>(\"POST\", path, body, { timeoutMs: (TASK_HOLD_SECONDS + 15) * 1000, signal: extra?.signal });\n } catch (err) {\n if (extra?.signal?.aborted) throw new McpError(-32603, \"canceled by the client\");\n throw new McpError(-32603, `Wakeshift: ${detailOf(err)}`);\n }\n if (out.rpc_error) throw new McpError(out.rpc_error.code, out.rpc_error.message);\n if (!out.result) throw new McpError(-32603, \"Wakeshift didn't answer for this task\");\n return out.result;\n };\n\n // server/discover (discover.ts) gets the tasks extension.\n const wrapDiscover = (h: Handler): Handler => async (request, extra) => {\n const out = await h(request, extra);\n if (out && typeof out === \"object\") {\n const caps = (out.capabilities ??= {});\n caps.extensions = { ...(caps.extensions ?? {}), [TASKS_EXTENSION]: {} };\n }\n return out;\n };\n // tools/list lists the wait tool for a request that takes tasks; tools/call\n // answers it. McpServer sets these when the first tool is registered,\n // perhaps after attach, so a later set is wrapped too.\n const wrapList = (h: Handler): Handler => async (request, extra) => {\n const out = await h(request, extra);\n if (takesTasks(request?.params?._meta, extra) && out && Array.isArray(out.tools) && !out.tools.some((t: any) => t?.name === WAIT_TOOL)) {\n out.tools = [...out.tools, WAIT_TOOL_DEFINITION];\n }\n return out;\n };\n const wrapCall = (h: Handler): Handler => async (request, extra) => (request?.params?.name === WAIT_TOOL ? waitCall(request, extra) : h(request, extra));\n // tasks/get and tasks/cancel: Wakeshift's own tasks (their IDs start\n // wkt1_) go to Wakeshift; any other goes to the app's own handler, if it\n // has one.\n const wrapTask = (path: string, hold: boolean) => (h: Handler | undefined): Handler => {\n const ours = taskRequest(path, hold);\n return async (request, extra) => {\n const id = request?.params?.taskId;\n if (typeof id === \"string\" && id.startsWith(TASK_ID_PREFIX)) return ours(request, extra);\n if (h) return h(request, extra);\n throw new McpError(-32601, \"Method not found\");\n };\n };\n const wrappers: Record<string, (h: Handler) => Handler> = {\n \"server/discover\": wrapDiscover,\n \"tools/list\": wrapList,\n \"tools/call\": wrapCall,\n \"tasks/get\": wrapTask(\"/calls/task/get\", true),\n \"tasks/cancel\": wrapTask(\"/calls/task/cancel\", false),\n };\n // Set directly, not through the MCP SDK's setRequestHandler, which would\n // ask for its own tasks capability (an older revision's), never advertised;\n // and once only (serveStateless wraps what's set through handlers.set).\n for (const [method, wrap] of Object.entries(wrappers)) {\n const h = handlers.get(method);\n if (h || method.startsWith(\"tasks/\")) Map.prototype.set.call(handlers, method, wrap(h as Handler));\n }\n const set = handlers.set.bind(handlers);\n handlers.set = (method: string, h: Handler) => set(method, wrappers[method] ? wrappers[method](h) : h);\n}\n\nfunction detailOf(err: unknown): string {\n return err instanceof WakeshiftError ? err.detail : String((err as Error)?.message ?? err);\n}\n"]}
@@ -4,7 +4,7 @@ import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
5
5
  import { type AnySchema, type SchemaInput, type ShapeOutput, type ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
6
6
  import type { WakeEvent } from "./events.js";
7
- import { type Duration, type Recognised } from "./wakeshift.js";
7
+ import { type Duration, type Recognized } from "./wakeshift.js";
8
8
  declare const SESSION_ARG = "session_key";
9
9
  declare const HARNESS_ARG = "harness";
10
10
  declare const CONVERSATION_ARG = "conversation_id";
@@ -44,8 +44,8 @@ export interface Subscription {
44
44
  created: boolean;
45
45
  /** The conversation's session. A secret: don't show, store or log it. */
46
46
  session: string;
47
- /** What Wakeshift recognised the agent's program as. */
48
- recognised: Recognised;
47
+ /** What Wakeshift recognized the agent's program as. */
48
+ recognized: Recognized;
49
49
  /** The caller subscribes itself (call.native): nothing was made, id is "", and the block tells it what to subscribe to. */
50
50
  native: boolean;
51
51
  }
@@ -78,8 +78,8 @@ export interface ToolCall {
78
78
  readonly nativeConversation: string | null;
79
79
  /** The session_key passed; after a subscribe or resolveSession, the conversation's session. */
80
80
  readonly session: string | null;
81
- /** What Wakeshift recognised the agent's program as: null until a subscribe or resolveSession. */
82
- readonly recognised: Recognised | null;
81
+ /** What Wakeshift recognized the agent's program as: null until a subscribe or resolveSession. */
82
+ readonly recognized: Recognized | null;
83
83
  /** MCP's own extra for the call. */
84
84
  readonly extra: ToolCallExtra;
85
85
  /**
@@ -120,6 +120,8 @@ export interface CallAddition {
120
120
  trailer?: string;
121
121
  meta?: Record<string, unknown>;
122
122
  }
123
+ /** The structuredContent field a wrapped tool's structured result carries Wakeshift's block in (the same in every SDK). */
124
+ export declare const WAKESHIFT_FIELD = "wakeshift";
123
125
  /**
124
126
  * Wakeshift attached to an MCP server: the handle `wake.attach` returns.
125
127
  * Wrap a tool's callback with events, and give its input schema
@@ -135,8 +137,10 @@ export declare class Attached {
135
137
  inputs<S extends ZodRawShapeCompat>(shape: S): S & WakeInputs;
136
138
  /**
137
139
  * shape plus an optional wakeshift string, for a wrapped tool's
138
- * outputSchema: the wrapper sets it to the block's text (without the
139
- * beacon line), for hosts that show the model only structuredContent.
140
+ * outputSchema. Optional: every structured result of a wrapped tool gets
141
+ * the block's text (without the beacon line) in wakeshift, for hosts that
142
+ * show the model only structuredContent, and tools/list names it in the
143
+ * tool's output schema; this declares it in your own schema and types.
140
144
  * The block stays in the content too.
141
145
  */
142
146
  output<S extends ZodRawShapeCompat>(shape: S): S & WakeOutputShape;
@@ -150,5 +154,6 @@ export declare class Attached {
150
154
  */
151
155
  events<S extends WakeShape>(name: string, handler: (args: AppArgs<S>, call: ToolCall) => WrappedResult | Promise<WrappedResult>): ToolCallback<S>;
152
156
  private run;
157
+ private amendTools;
153
158
  }
154
159
  export {};
package/dist/toolcall.js CHANGED
@@ -21,6 +21,7 @@ import * as z4 from "zod/v4";
21
21
  import { getObjectShape, isZ4Schema, normalizeObjectSchema, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
22
22
  import { WakeshiftError } from "./errors.js";
23
23
  import { EXTENSION_KEY, dialectOf } from "./spec.js";
24
+ import { takesTasks } from "./discover.js";
24
25
  import { packCall } from "./wakeshift.js";
25
26
  // Wakeshift's three arguments (wakeshift.ts's SESSION_ARG, HARNESS_ARG and
26
27
  // CONVERSATION_ARG, repeated: that module imports this one).
@@ -33,6 +34,8 @@ const SESSION_KEY_DESCRIPTION = "This conversation's session_key, from an earlie
33
34
  const HARNESS_DESCRIPTION = "The program you're running in: codex, claude-code, cursor (local CLI or IDE), cursor-cloud (Cursor cloud agent), grok-bot, copilot-cli, opencode, antigravity, openclaw (even when it runs your turns in Codex), hermes, chatgpt, or your program's name if it isn't listed";
34
35
  const CONVERSATION_DESCRIPTION = "Your own ID for this conversation, if your program gives you one. Cursor and Grok Bot: the output of `printenv CURSOR_CONVERSATION_ID`. Codex and Antigravity send it automatically; others leave it empty";
35
36
  const WAKESHIFT_OUTPUT_DESCRIPTION = "What Wakeshift adds for this conversation: how it's woken by this app's events";
37
+ /** @internal Wakeshift's three arguments' descriptions, for a tool whose input schema is written out (the wait tool, tasks.ts). */
38
+ export const WAKE_ARG_DESCRIPTIONS = { session_key: SESSION_KEY_DESCRIPTION, harness: HARNESS_DESCRIPTION, conversation_id: CONVERSATION_DESCRIPTION };
36
39
  const wakeInputs = {
37
40
  [SESSION_ARG]: z.string().optional().describe(SESSION_KEY_DESCRIPTION),
38
41
  [HARNESS_ARG]: z.string().optional().describe(HARNESS_DESCRIPTION),
@@ -114,15 +117,38 @@ function takeTrailer(content) {
114
117
  content[content.length - 1] = { ...last, text: rest };
115
118
  return line;
116
119
  }
120
+ /** The structuredContent field a wrapped tool's structured result carries Wakeshift's block in (the same in every SDK). */
121
+ export const WAKESHIFT_FIELD = "wakeshift";
122
+ /**
123
+ * @internal An object output schema (JSON Schema, as tools/list gives it)
124
+ * with an optional wakeshift string added, as a copy, so a client that
125
+ * checks structuredContent against it takes the block. Anything else, or a
126
+ * schema that already names it, comes back as it was.
127
+ * test/wrapped-block.json (output_schemas) pins it.
128
+ */
129
+ export function withWakeshiftField(schema) {
130
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
131
+ return schema;
132
+ const s = schema;
133
+ if (s.type !== "object")
134
+ return schema;
135
+ const props = s.properties;
136
+ if (props !== undefined && (!props || typeof props !== "object" || Array.isArray(props)))
137
+ return schema;
138
+ if (props && WAKESHIFT_FIELD in props)
139
+ return schema;
140
+ return { ...s, properties: { ...(props ?? {}), [WAKESHIFT_FIELD]: { type: "string", description: WAKESHIFT_OUTPUT_DESCRIPTION } } };
141
+ }
117
142
  /**
118
143
  * @internal Adds the call's block to a wrapped handler's result: Wakeshift's
119
144
  * text as a block after the app's content, then a text-carried beacon last,
120
145
  * and _meta; a string result stays one text block. A result that already
121
- * carries a beacon keeps it, last, and gets no second one. With wakeOutput,
122
- * structuredContent's wakeshift is the block's text. test/wrapped-block.json
123
- * is the shape, shared with the other SDKs.
146
+ * carries a beacon keeps it, last, and gets no second one. A structured
147
+ * result (structuredContent an object) gets the block's text in its
148
+ * wakeshift field too. test/wrapped-block.json is the shape, shared with
149
+ * the other SDKs.
124
150
  */
125
- export function mergeBlock(result, add, wakeOutput) {
151
+ export function mergeBlock(result, add) {
126
152
  const isString = typeof result === "string";
127
153
  const base = isString ? { content: [{ type: "text", text: result }] } : result;
128
154
  const out = { ...base, content: [...(base.content ?? [])] };
@@ -145,8 +171,8 @@ export function mergeBlock(result, add, wakeOutput) {
145
171
  const texts = out.content.map((c) => (c.type === "text" ? c.text : "")).filter((t) => t !== "");
146
172
  out.content = [{ type: "text", text: texts.join("\n\n") }];
147
173
  }
148
- if (text && wakeOutput && out.structuredContent && typeof out.structuredContent === "object" && !Array.isArray(out.structuredContent)) {
149
- out.structuredContent = { ...out.structuredContent, wakeshift: text };
174
+ if (text && out.structuredContent && typeof out.structuredContent === "object" && !Array.isArray(out.structuredContent)) {
175
+ out.structuredContent = { ...out.structuredContent, [WAKESHIFT_FIELD]: text };
150
176
  }
151
177
  return out;
152
178
  }
@@ -198,7 +224,7 @@ class CallScope {
198
224
  native = false;
199
225
  nativeConversation = null;
200
226
  session;
201
- recognised = null;
227
+ recognized = null;
202
228
  extra;
203
229
  ended = false;
204
230
  pending = "";
@@ -236,6 +262,7 @@ class CallScope {
236
262
  harness: this.args.harness,
237
263
  conversation: this.args.conversation,
238
264
  tool: this.tool,
265
+ tasks: takesTasks(meta, this.extra),
239
266
  });
240
267
  }
241
268
  // request runs one request of the call to Wakeshift, after any before it
@@ -260,7 +287,7 @@ class CallScope {
260
287
  const known = event.parseArgs(args);
261
288
  if (this.native) {
262
289
  this.addNative(event.name, known);
263
- return { id: "", user: this.user ?? "", created: false, session: "", recognised: { harness: null, via: null, claimed: null }, native: true };
290
+ return { id: "", user: this.user ?? "", created: false, session: "", recognized: { harness: null, via: null, claimed: null }, native: true };
264
291
  }
265
292
  if (event.personal && !this.user) {
266
293
  throw new WakeshiftError(400, "bad_arguments", `${event.name} is personal: call.subscribe needs a user (attach's identify), who the subscription belongs to`);
@@ -279,12 +306,12 @@ class CallScope {
279
306
  this.pending = out.pending;
280
307
  this.armed = true;
281
308
  this.session = out.session;
282
- const rec = recognisedOf(out.agent);
283
- this.recognised = rec;
309
+ const rec = recognizedOf(out.agent);
310
+ this.recognized = rec;
284
311
  const es = out.event_subscription;
285
312
  if (es.created)
286
313
  this.created.push({ name: event.name, arguments: known, session: out.session, pending: out.pending ?? this.pending });
287
- return { id: es.id, user: es.user ?? this.user ?? "", created: !!es.created, session: out.session, recognised: rec, native: false };
314
+ return { id: es.id, user: es.user ?? this.user ?? "", created: !!es.created, session: out.session, recognized: rec, native: false };
288
315
  });
289
316
  }
290
317
  unsubscribe(event, args) {
@@ -304,7 +331,7 @@ class CallScope {
304
331
  const out = await this.at.wake.requestApp("POST", "/calls/session", { call: this.wire() });
305
332
  this.pending = out.pending;
306
333
  this.session = out.session;
307
- this.recognised = recognisedOf(out.agent);
334
+ this.recognized = recognizedOf(out.agent);
308
335
  return out.session;
309
336
  });
310
337
  }
@@ -385,7 +412,7 @@ class CallScope {
385
412
  }
386
413
  }
387
414
  }
388
- function recognisedOf(a) {
415
+ function recognizedOf(a) {
389
416
  return { harness: a?.harness ?? null, via: a?.via ?? null, claimed: a?.claimed ?? null };
390
417
  }
391
418
  function headerOf(extra, name) {
@@ -453,8 +480,10 @@ export class Attached {
453
480
  }
454
481
  /**
455
482
  * shape plus an optional wakeshift string, for a wrapped tool's
456
- * outputSchema: the wrapper sets it to the block's text (without the
457
- * beacon line), for hosts that show the model only structuredContent.
483
+ * outputSchema. Optional: every structured result of a wrapped tool gets
484
+ * the block's text (without the beacon line) in wakeshift, for hosts that
485
+ * show the model only structuredContent, and tools/list names it in the
486
+ * tool's output schema; this declares it in your own schema and types.
458
487
  * The block stays in the content too.
459
488
  */
460
489
  output(shape) {
@@ -484,7 +513,6 @@ export class Attached {
484
513
  if (tool === undefined || (tool && (tool.handler ?? tool.callback) !== cb)) {
485
514
  throw new Error(`wakeshift: attached.events("${name}", …) isn't the tool named ${name} on the server passed to attach: register it with the same name, on that server`);
486
515
  }
487
- let wakeOutput = false;
488
516
  let outputSchema;
489
517
  if (tool) {
490
518
  const shape = getObjectShape(normalizeObjectSchema(tool.inputSchema));
@@ -492,8 +520,6 @@ export class Attached {
492
520
  throw new Error(`wakeshift: the tool ${name} doesn't take Wakeshift's arguments: make its inputSchema with attached.inputs(…)`);
493
521
  }
494
522
  outputSchema = tool.outputSchema;
495
- const out = getObjectShape(normalizeObjectSchema(outputSchema));
496
- wakeOutput = !!out && "wakeshift" in out;
497
523
  }
498
524
  else {
499
525
  this.at.warnOnce("internals", "wakeshift: this MCP SDK keeps its tools where Wakeshift can't read them, so wrapped tools' registration and output aren't checked");
@@ -536,12 +562,48 @@ export class Attached {
536
562
  this.at.wake.warn(`wakeshift: Wakeshift's block for a result of ${name} failed, so the call's subscriptions were rolled back: ${detailOf(err)}`);
537
563
  return { isError: true, content: [{ type: "text", text: `Wakeshift couldn't finish setting up this conversation's wakes (${detailOf(err)}). Try again.` }] };
538
564
  }
539
- const out = mergeBlock(result, add ?? {}, wakeOutput);
540
- if ((add?.text ?? "").trim() && !wakeOutput && out.structuredContent) {
541
- this.at.warnOnce("output:" + name, `wakeshift: the wrapped tool ${name} returns structuredContent without attached.output: hosts that show only structuredContent won't see how this conversation is woken`);
565
+ const out = mergeBlock(result, add ?? {});
566
+ if (out.structuredContent && WAKESHIFT_FIELD in out.structuredContent && !(await validOutput(outputSchema, out))) {
567
+ // A schema that refuses fields it doesn't name (a strict object):
568
+ // MCP would refuse the result, so it goes without the field.
569
+ const { [WAKESHIFT_FIELD]: _dropped, ...own } = out.structuredContent;
570
+ out.structuredContent = own;
571
+ this.at.warnOnce("output:" + name, `wakeshift: the wrapped tool ${name}'s outputSchema refuses a wakeshift field, so hosts that show only structuredContent won't see how this conversation is woken: make it with attached.output`);
542
572
  }
543
573
  return out;
544
574
  }
575
+ /**
576
+ * @internal Wraps the server's tools/list handler so each wrapped tool's
577
+ * object output schema names wakeshift (withWakeshiftField). McpServer
578
+ * sets the handler when the first tool is registered, perhaps after
579
+ * attach, so a later set is wrapped too (as tasks.ts does).
580
+ */
581
+ amendToolsList() {
582
+ const handlers = this.at.server.server._requestHandlers;
583
+ if (!(handlers instanceof Map))
584
+ return; // a future SDK: schemas stay as registered
585
+ const wrap = (h) => async (request, extra) => {
586
+ const out = await h(request, extra);
587
+ if (out && Array.isArray(out.tools))
588
+ this.amendTools(out.tools);
589
+ return out;
590
+ };
591
+ const h = handlers.get("tools/list");
592
+ if (h)
593
+ Map.prototype.set.call(handlers, "tools/list", wrap(h));
594
+ const set = handlers.set.bind(handlers);
595
+ handlers.set = (method, h) => set(method, method === "tools/list" ? wrap(h) : h);
596
+ }
597
+ amendTools(tools) {
598
+ for (let i = 0; i < tools.length; i++) {
599
+ const t = tools[i];
600
+ if (!t || typeof t !== "object" || typeof t.name !== "string" || !this.wrapped.has(t.name) || !t.outputSchema)
601
+ continue;
602
+ const schema = withWakeshiftField(t.outputSchema);
603
+ if (schema !== t.outputSchema)
604
+ tools[i] = { ...t, outputSchema: schema };
605
+ }
606
+ }
545
607
  }
546
608
  // validOutput mirrors McpServer's own check of a result against the tool's
547
609
  // outputSchema (validateToolOutput): one it would refuse fails the call.