@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/CHANGELOG.md +39 -4
- package/README.md +9 -9
- package/dist/discover.d.ts +4 -0
- package/dist/discover.js +107 -0
- package/dist/discover.js.map +1 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.js +64 -17
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/spec.d.ts +4 -2
- package/dist/spec.js +4 -2
- package/dist/spec.js.map +1 -1
- package/dist/tasks.d.ts +33 -0
- package/dist/tasks.js +147 -0
- package/dist/tasks.js.map +1 -0
- package/dist/toolcall.d.ts +12 -7
- package/dist/toolcall.js +83 -21
- package/dist/toolcall.js.map +1 -1
- package/dist/wakeshift.d.ts +10 -7
- package/dist/wakeshift.js +53 -32
- package/dist/wakeshift.js.map +1 -1
- package/package.json +1 -1
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"]}
|
package/dist/toolcall.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
48
|
-
|
|
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
|
|
82
|
-
readonly
|
|
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:
|
|
139
|
-
* beacon line), for hosts that
|
|
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.
|
|
122
|
-
* structuredContent
|
|
123
|
-
* is the shape, shared with
|
|
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
|
|
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 &&
|
|
149
|
-
out.structuredContent = { ...out.structuredContent,
|
|
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
|
-
|
|
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: "",
|
|
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 =
|
|
283
|
-
this.
|
|
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,
|
|
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.
|
|
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
|
|
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:
|
|
457
|
-
* beacon line), for hosts that
|
|
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 ?? {}
|
|
540
|
-
if (
|
|
541
|
-
|
|
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.
|