@zetaloop/chappie 0.2.1 → 0.3.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 +27 -7
- package/docs/tools.md +55 -74
- package/package.json +3 -1
- package/src/broker.ts +193 -35
- package/src/config.ts +21 -0
- package/src/delivery.ts +25 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +7 -5
- package/src/ipc.ts +142 -40
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +206 -62
- package/src/session.ts +111 -41
- package/src/state.ts +98 -7
- package/src/stdio.ts +11 -2
- package/src/tools.ts +3 -22
- package/src/transfer.ts +3 -3
package/src/server.ts
CHANGED
|
@@ -4,6 +4,13 @@ import * as z from "zod";
|
|
|
4
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
5
5
|
import type { Broker } from "./broker.ts";
|
|
6
6
|
import { deliveryContent } from "./delivery.ts";
|
|
7
|
+
import {
|
|
8
|
+
answerContent,
|
|
9
|
+
answerInput,
|
|
10
|
+
questionInput,
|
|
11
|
+
questionInstructions,
|
|
12
|
+
questionOutput,
|
|
13
|
+
} from "./questions.ts";
|
|
7
14
|
import {
|
|
8
15
|
directTools,
|
|
9
16
|
inputContent,
|
|
@@ -20,10 +27,19 @@ const outputSchema = z.object({
|
|
|
20
27
|
text: z
|
|
21
28
|
.string()
|
|
22
29
|
.describe(
|
|
23
|
-
"Complete text output, including Pi user input and deferred results. Images and file resources accompany it as native content blocks.",
|
|
30
|
+
"Complete text output, including Pi user input, submitted webpage answers, and deferred results. Images and file resources accompany it as native content blocks.",
|
|
24
31
|
),
|
|
25
32
|
});
|
|
26
33
|
|
|
34
|
+
const questionTemplate = "ui://chappie/question.html";
|
|
35
|
+
const questionSchema = outputSchema.extend({ question: questionOutput });
|
|
36
|
+
const toolAnnotations = {
|
|
37
|
+
readOnlyHint: true,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
idempotentHint: true,
|
|
40
|
+
openWorldHint: false,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
27
43
|
interface RequestContext {
|
|
28
44
|
mcpReq: {
|
|
29
45
|
_meta?: Record<string, unknown>;
|
|
@@ -37,31 +53,45 @@ export function createServer(broker: Broker): McpServer {
|
|
|
37
53
|
name: "chappie",
|
|
38
54
|
version: packageJson.version,
|
|
39
55
|
},
|
|
40
|
-
{
|
|
56
|
+
{
|
|
57
|
+
instructions: broker.askEnabled
|
|
58
|
+
? `${instructions}\n\n${questionInstructions}`
|
|
59
|
+
: instructions,
|
|
60
|
+
},
|
|
41
61
|
);
|
|
42
62
|
|
|
63
|
+
function handle<Args, Result>(
|
|
64
|
+
callback: (args: Args, context: RequestContext) => Promise<Result>,
|
|
65
|
+
) {
|
|
66
|
+
return async (args: Args, context: RequestContext): Promise<Result> => {
|
|
67
|
+
const signal = await broker.workflow(
|
|
68
|
+
requestChatId(context),
|
|
69
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
70
|
+
context.mcpReq.signal,
|
|
71
|
+
);
|
|
72
|
+
return callback(args, {
|
|
73
|
+
...context,
|
|
74
|
+
mcpReq: { ...context.mcpReq, signal },
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
server.registerTool(
|
|
44
80
|
"init",
|
|
45
81
|
{
|
|
46
82
|
title: "Connect to Pi",
|
|
47
83
|
description:
|
|
48
|
-
"
|
|
84
|
+
"Select this chat's default Pi session and return its environment and tool catalog. Use the task's sessionId to resume, or find it by cwd/name with sessions. For a task without a specified target, omit sessionId to reuse the default or select the first online, unbound session.",
|
|
49
85
|
outputSchema,
|
|
50
86
|
inputSchema: z.object({
|
|
51
87
|
sessionId: z
|
|
52
88
|
.string()
|
|
53
89
|
.optional()
|
|
54
|
-
.describe(
|
|
55
|
-
"Pi session ID to set as this chat's default, including an already-bound session when resuming work",
|
|
56
|
-
),
|
|
90
|
+
.describe("Default Pi session ID; may be shared with other chats"),
|
|
57
91
|
}),
|
|
58
|
-
annotations:
|
|
59
|
-
readOnlyHint: false,
|
|
60
|
-
destructiveHint: false,
|
|
61
|
-
openWorldHint: false,
|
|
62
|
-
},
|
|
92
|
+
annotations: toolAnnotations,
|
|
63
93
|
},
|
|
64
|
-
async (args, context) => {
|
|
94
|
+
handle(async (args, context) => {
|
|
65
95
|
const chatId = requireChatId(context);
|
|
66
96
|
const { inputs, ...initialized } = await broker.initialize(
|
|
67
97
|
chatId,
|
|
@@ -69,7 +99,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
69
99
|
context.mcpReq.signal,
|
|
70
100
|
);
|
|
71
101
|
return finishResult(broker, context, textResult(initialized, inputs));
|
|
72
|
-
},
|
|
102
|
+
}),
|
|
73
103
|
);
|
|
74
104
|
|
|
75
105
|
server.registerTool(
|
|
@@ -77,25 +107,18 @@ export function createServer(broker: Broker): McpServer {
|
|
|
77
107
|
{
|
|
78
108
|
title: "Reply in Pi",
|
|
79
109
|
description:
|
|
80
|
-
"
|
|
110
|
+
"Send an assistant message to Pi. Renders Markdown and saves the message in the session transcript.",
|
|
81
111
|
outputSchema,
|
|
82
112
|
inputSchema: z.object({
|
|
83
|
-
text: z
|
|
84
|
-
.string()
|
|
85
|
-
.min(1)
|
|
86
|
-
.describe("Markdown message, including prose and code examples"),
|
|
113
|
+
text: z.string().min(1).describe("Assistant message in Markdown"),
|
|
87
114
|
sessionId: z
|
|
88
115
|
.string()
|
|
89
116
|
.optional()
|
|
90
117
|
.describe("Pi session for this operation only"),
|
|
91
118
|
}),
|
|
92
|
-
annotations:
|
|
93
|
-
readOnlyHint: false,
|
|
94
|
-
destructiveHint: false,
|
|
95
|
-
openWorldHint: false,
|
|
96
|
-
},
|
|
119
|
+
annotations: toolAnnotations,
|
|
97
120
|
},
|
|
98
|
-
async (args, context) => {
|
|
121
|
+
handle(async (args, context) => {
|
|
99
122
|
const chatId = requireChatId(context);
|
|
100
123
|
const { sessionId, cwd, inputs } = await broker.chat(
|
|
101
124
|
chatId,
|
|
@@ -108,15 +131,150 @@ export function createServer(broker: Broker): McpServer {
|
|
|
108
131
|
context,
|
|
109
132
|
textResult({ sessionId, cwd }, inputs),
|
|
110
133
|
);
|
|
111
|
-
},
|
|
134
|
+
}),
|
|
112
135
|
);
|
|
113
136
|
|
|
137
|
+
if (broker.askEnabled) {
|
|
138
|
+
server.registerTool(
|
|
139
|
+
"ask",
|
|
140
|
+
{
|
|
141
|
+
title: "Ask in ChatGPT",
|
|
142
|
+
description:
|
|
143
|
+
"Create a question in ChatGPT and return its ID immediately. Call ask_assert next with question.id. Answers, revisions, and skips arrive as webAnswer in later tool results.",
|
|
144
|
+
inputSchema: questionInput.extend({
|
|
145
|
+
sessionId: z
|
|
146
|
+
.string()
|
|
147
|
+
.optional()
|
|
148
|
+
.describe(
|
|
149
|
+
"Pi session for this question; defaults to this chat's session",
|
|
150
|
+
),
|
|
151
|
+
}),
|
|
152
|
+
outputSchema: questionSchema,
|
|
153
|
+
annotations: toolAnnotations,
|
|
154
|
+
_meta: { ui: { resourceUri: questionTemplate } },
|
|
155
|
+
},
|
|
156
|
+
handle(async ({ sessionId, ...input }, context) => {
|
|
157
|
+
const question = await broker.ask(
|
|
158
|
+
requireChatId(context),
|
|
159
|
+
sessionId,
|
|
160
|
+
input,
|
|
161
|
+
context.mcpReq.signal,
|
|
162
|
+
);
|
|
163
|
+
const result = await finishResult(broker, context, {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: `Question created. Call ask_assert({"questionId":"${question.id}"}) next.`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
...result,
|
|
173
|
+
structuredContent: { ...result.structuredContent, question },
|
|
174
|
+
};
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
server.registerTool(
|
|
179
|
+
"ask_assert",
|
|
180
|
+
{
|
|
181
|
+
title: "Assert question display",
|
|
182
|
+
description:
|
|
183
|
+
"Assert that an ask widget loaded in ChatGPT. Call immediately after ask with question.id. Returns when the widget reports loaded; times out if it fails to load. User answers arrive separately as webAnswer.",
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
questionId: z.string().describe("question.id returned by ask"),
|
|
186
|
+
}),
|
|
187
|
+
outputSchema: questionSchema,
|
|
188
|
+
annotations: toolAnnotations,
|
|
189
|
+
},
|
|
190
|
+
handle(async ({ questionId }, context) => {
|
|
191
|
+
const question = await broker.assertQuestion(
|
|
192
|
+
requireChatId(context),
|
|
193
|
+
questionId,
|
|
194
|
+
context.mcpReq.signal,
|
|
195
|
+
);
|
|
196
|
+
const result = await finishResult(broker, context, {
|
|
197
|
+
content: [{ type: "text", text: "Question widget loaded." }],
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
...result,
|
|
201
|
+
structuredContent: { ...result.structuredContent, question },
|
|
202
|
+
};
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
server.registerTool(
|
|
207
|
+
"answer",
|
|
208
|
+
{
|
|
209
|
+
title: "Question state",
|
|
210
|
+
description:
|
|
211
|
+
"Read a saved question, report widget loading, or save an answer, revision, or skip.",
|
|
212
|
+
inputSchema: z.object({
|
|
213
|
+
questionId: z.string(),
|
|
214
|
+
answer: answerInput.optional(),
|
|
215
|
+
loaded: z
|
|
216
|
+
.literal(true)
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("The question widget has loaded"),
|
|
219
|
+
}),
|
|
220
|
+
outputSchema: questionSchema,
|
|
221
|
+
annotations: toolAnnotations,
|
|
222
|
+
_meta: { ui: { visibility: ["app"] }, "openai/widgetAccessible": true },
|
|
223
|
+
},
|
|
224
|
+
handle(async ({ questionId, answer, loaded = false }, context) => {
|
|
225
|
+
const question = await broker.answer(
|
|
226
|
+
requireChatId(context),
|
|
227
|
+
questionId,
|
|
228
|
+
answer,
|
|
229
|
+
loaded,
|
|
230
|
+
);
|
|
231
|
+
const text = answer
|
|
232
|
+
? answer.skipped
|
|
233
|
+
? "Question skipped."
|
|
234
|
+
: "Answer saved."
|
|
235
|
+
: loaded
|
|
236
|
+
? "Question widget loaded."
|
|
237
|
+
: "Question state.";
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text }],
|
|
240
|
+
structuredContent: { text, question },
|
|
241
|
+
};
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
server.registerResource(
|
|
246
|
+
"question",
|
|
247
|
+
questionTemplate,
|
|
248
|
+
{ title: "Chappie question", mimeType: "text/html;profile=mcp-app" },
|
|
249
|
+
async () => ({
|
|
250
|
+
contents: [
|
|
251
|
+
{
|
|
252
|
+
uri: questionTemplate,
|
|
253
|
+
mimeType: "text/html;profile=mcp-app",
|
|
254
|
+
text: readFileSync(
|
|
255
|
+
new URL("./question.html", import.meta.url),
|
|
256
|
+
"utf8",
|
|
257
|
+
),
|
|
258
|
+
_meta: {
|
|
259
|
+
ui: {
|
|
260
|
+
prefersBorder: true,
|
|
261
|
+
csp: { connectDomains: [], resourceDomains: [] },
|
|
262
|
+
},
|
|
263
|
+
"openai/widgetDescription":
|
|
264
|
+
"A persistent question the user can answer while the assistant continues working.",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
114
272
|
server.registerTool(
|
|
115
273
|
"tools",
|
|
116
274
|
{
|
|
117
275
|
title: "Pi tools",
|
|
118
276
|
description:
|
|
119
|
-
"
|
|
277
|
+
"Get full definitions of Pi tools for call. Filter by names, or omit names to list all active tools.",
|
|
120
278
|
outputSchema,
|
|
121
279
|
inputSchema: z.object({
|
|
122
280
|
names: z
|
|
@@ -129,14 +287,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
129
287
|
.optional()
|
|
130
288
|
.describe("Pi session for this operation only"),
|
|
131
289
|
}),
|
|
132
|
-
annotations:
|
|
133
|
-
readOnlyHint: true,
|
|
134
|
-
destructiveHint: false,
|
|
135
|
-
idempotentHint: true,
|
|
136
|
-
openWorldHint: false,
|
|
137
|
-
},
|
|
290
|
+
annotations: toolAnnotations,
|
|
138
291
|
},
|
|
139
|
-
async (args, context) => {
|
|
292
|
+
handle(async (args, context) => {
|
|
140
293
|
const { inputs, ...inspected } = await broker.tools(
|
|
141
294
|
requireChatId(context),
|
|
142
295
|
args.sessionId,
|
|
@@ -151,7 +304,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
151
304
|
inputs,
|
|
152
305
|
),
|
|
153
306
|
);
|
|
154
|
-
},
|
|
307
|
+
}),
|
|
155
308
|
);
|
|
156
309
|
|
|
157
310
|
server.registerTool(
|
|
@@ -159,7 +312,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
159
312
|
{
|
|
160
313
|
title: "Call Pi tools",
|
|
161
314
|
description:
|
|
162
|
-
"Execute
|
|
315
|
+
"Execute Pi tools using the definitions returned by tools. Each calls array is one native Pi batch.",
|
|
163
316
|
outputSchema,
|
|
164
317
|
inputSchema: z.object({
|
|
165
318
|
calls: z
|
|
@@ -175,13 +328,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
175
328
|
.optional()
|
|
176
329
|
.describe("Pi session for this operation only"),
|
|
177
330
|
}),
|
|
178
|
-
annotations:
|
|
179
|
-
readOnlyHint: false,
|
|
180
|
-
destructiveHint: true,
|
|
181
|
-
openWorldHint: true,
|
|
182
|
-
},
|
|
331
|
+
annotations: toolAnnotations,
|
|
183
332
|
},
|
|
184
|
-
async (args, context) => {
|
|
333
|
+
handle(async (args, context) => {
|
|
185
334
|
const result = await broker.call(
|
|
186
335
|
requireChatId(context),
|
|
187
336
|
args.sessionId,
|
|
@@ -198,7 +347,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
198
347
|
result.inputs,
|
|
199
348
|
),
|
|
200
349
|
);
|
|
201
|
-
},
|
|
350
|
+
}),
|
|
202
351
|
);
|
|
203
352
|
|
|
204
353
|
for (const tool of directTools) {
|
|
@@ -209,17 +358,12 @@ export function createServer(broker: Broker): McpServer {
|
|
|
209
358
|
description: tool.description,
|
|
210
359
|
outputSchema,
|
|
211
360
|
inputSchema: tool.inputSchema,
|
|
212
|
-
annotations:
|
|
213
|
-
readOnlyHint: tool.name === "read",
|
|
214
|
-
destructiveHint: tool.name !== "read",
|
|
215
|
-
idempotentHint: tool.name === "read",
|
|
216
|
-
openWorldHint: tool.name === "bash" || tool.name === "transfer",
|
|
217
|
-
},
|
|
361
|
+
annotations: toolAnnotations,
|
|
218
362
|
...(tool.fileParams
|
|
219
363
|
? { _meta: { "openai/fileParams": tool.fileParams } }
|
|
220
364
|
: {}),
|
|
221
365
|
},
|
|
222
|
-
async (args, context) => {
|
|
366
|
+
handle(async (args, context) => {
|
|
223
367
|
const input = { ...args } as Record<string, unknown> & {
|
|
224
368
|
sessionId?: string;
|
|
225
369
|
};
|
|
@@ -242,7 +386,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
242
386
|
result.inputs,
|
|
243
387
|
),
|
|
244
388
|
);
|
|
245
|
-
},
|
|
389
|
+
}),
|
|
246
390
|
);
|
|
247
391
|
}
|
|
248
392
|
|
|
@@ -251,22 +395,17 @@ export function createServer(broker: Broker): McpServer {
|
|
|
251
395
|
{
|
|
252
396
|
title: "Local sessions",
|
|
253
397
|
description:
|
|
254
|
-
"List online Pi sessions
|
|
398
|
+
"List online Pi sessions with their IDs, devices, cwd, names, execution status, and saved binding counts. Also returns this chat's default.",
|
|
255
399
|
outputSchema,
|
|
256
400
|
inputSchema: z.object({
|
|
257
401
|
sessionId: z
|
|
258
402
|
.string()
|
|
259
403
|
.optional()
|
|
260
|
-
.describe("
|
|
404
|
+
.describe("Filter the online list to this Pi session"),
|
|
261
405
|
}),
|
|
262
|
-
annotations:
|
|
263
|
-
readOnlyHint: true,
|
|
264
|
-
destructiveHint: false,
|
|
265
|
-
idempotentHint: true,
|
|
266
|
-
openWorldHint: false,
|
|
267
|
-
},
|
|
406
|
+
annotations: toolAnnotations,
|
|
268
407
|
},
|
|
269
|
-
async (args, context) => {
|
|
408
|
+
handle(async (args, context) => {
|
|
270
409
|
const chatId = requestChatId(context);
|
|
271
410
|
const inputs = chatId
|
|
272
411
|
? await broker.inputs(chatId, args.sessionId, context.mcpReq.signal)
|
|
@@ -279,7 +418,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
279
418
|
inputs,
|
|
280
419
|
);
|
|
281
420
|
return finishResult(broker, context, result);
|
|
282
|
-
},
|
|
421
|
+
}),
|
|
283
422
|
);
|
|
284
423
|
|
|
285
424
|
server.registerResource(
|
|
@@ -325,13 +464,18 @@ async function finishResult<
|
|
|
325
464
|
>(broker: Broker, context: RequestContext, result: T) {
|
|
326
465
|
const chatId = requestChatId(context);
|
|
327
466
|
const deliveries = chatId ? await broker.deliveries(chatId) : [];
|
|
328
|
-
const
|
|
467
|
+
const answers = chatId ? broker.answers(chatId) : [];
|
|
468
|
+
const content = [
|
|
469
|
+
...result.content,
|
|
470
|
+
...deliveryContent(deliveries),
|
|
471
|
+
...answerContent(answers),
|
|
472
|
+
];
|
|
329
473
|
const structuredContent = {
|
|
330
474
|
text: content
|
|
331
475
|
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
332
476
|
.join("\n"),
|
|
333
477
|
};
|
|
334
|
-
await broker.
|
|
478
|
+
await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
|
|
335
479
|
return { ...result, content, structuredContent };
|
|
336
480
|
}
|
|
337
481
|
|