@zetaloop/chappie 0.2.0 → 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 -68
- package/package.json +3 -1
- package/src/broker.ts +240 -50
- package/src/config.ts +21 -0
- package/src/delivery.ts +27 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +8 -4
- package/src/ipc.ts +144 -41
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +250 -69
- package/src/session.ts +131 -67
- package/src/state.ts +104 -9
- package/src/stdio.ts +11 -2
- package/src/tools.ts +6 -23
- 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,
|
|
@@ -16,6 +23,23 @@ const instructions = readFileSync(
|
|
|
16
23
|
"utf8",
|
|
17
24
|
).trim();
|
|
18
25
|
|
|
26
|
+
const outputSchema = z.object({
|
|
27
|
+
text: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe(
|
|
30
|
+
"Complete text output, including Pi user input, submitted webpage answers, and deferred results. Images and file resources accompany it as native content blocks.",
|
|
31
|
+
),
|
|
32
|
+
});
|
|
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
|
+
|
|
19
43
|
interface RequestContext {
|
|
20
44
|
mcpReq: {
|
|
21
45
|
_meta?: Record<string, unknown>;
|
|
@@ -29,27 +53,45 @@ export function createServer(broker: Broker): McpServer {
|
|
|
29
53
|
name: "chappie",
|
|
30
54
|
version: packageJson.version,
|
|
31
55
|
},
|
|
32
|
-
{
|
|
56
|
+
{
|
|
57
|
+
instructions: broker.askEnabled
|
|
58
|
+
? `${instructions}\n\n${questionInstructions}`
|
|
59
|
+
: instructions,
|
|
60
|
+
},
|
|
33
61
|
);
|
|
34
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
|
+
|
|
35
79
|
server.registerTool(
|
|
36
80
|
"init",
|
|
37
81
|
{
|
|
38
82
|
title: "Connect to Pi",
|
|
39
83
|
description:
|
|
40
|
-
"
|
|
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.",
|
|
85
|
+
outputSchema,
|
|
41
86
|
inputSchema: z.object({
|
|
42
87
|
sessionId: z
|
|
43
88
|
.string()
|
|
44
89
|
.optional()
|
|
45
|
-
.describe("Pi session
|
|
90
|
+
.describe("Default Pi session ID; may be shared with other chats"),
|
|
46
91
|
}),
|
|
47
|
-
annotations:
|
|
48
|
-
destructiveHint: false,
|
|
49
|
-
openWorldHint: false,
|
|
50
|
-
},
|
|
92
|
+
annotations: toolAnnotations,
|
|
51
93
|
},
|
|
52
|
-
async (args, context) => {
|
|
94
|
+
handle(async (args, context) => {
|
|
53
95
|
const chatId = requireChatId(context);
|
|
54
96
|
const { inputs, ...initialized } = await broker.initialize(
|
|
55
97
|
chatId,
|
|
@@ -57,44 +99,183 @@ export function createServer(broker: Broker): McpServer {
|
|
|
57
99
|
context.mcpReq.signal,
|
|
58
100
|
);
|
|
59
101
|
return finishResult(broker, context, textResult(initialized, inputs));
|
|
60
|
-
},
|
|
102
|
+
}),
|
|
61
103
|
);
|
|
62
104
|
|
|
63
105
|
server.registerTool(
|
|
64
106
|
"chat",
|
|
65
107
|
{
|
|
66
108
|
title: "Reply in Pi",
|
|
67
|
-
description:
|
|
109
|
+
description:
|
|
110
|
+
"Send an assistant message to Pi. Renders Markdown and saves the message in the session transcript.",
|
|
111
|
+
outputSchema,
|
|
68
112
|
inputSchema: z.object({
|
|
69
|
-
text: z.string().min(1).describe("Assistant message
|
|
113
|
+
text: z.string().min(1).describe("Assistant message in Markdown"),
|
|
70
114
|
sessionId: z
|
|
71
115
|
.string()
|
|
72
116
|
.optional()
|
|
73
117
|
.describe("Pi session for this operation only"),
|
|
74
118
|
}),
|
|
75
|
-
annotations:
|
|
76
|
-
destructiveHint: false,
|
|
77
|
-
openWorldHint: false,
|
|
78
|
-
},
|
|
119
|
+
annotations: toolAnnotations,
|
|
79
120
|
},
|
|
80
|
-
async (args, context) => {
|
|
121
|
+
handle(async (args, context) => {
|
|
81
122
|
const chatId = requireChatId(context);
|
|
82
|
-
const { sessionId, inputs } = await broker.chat(
|
|
123
|
+
const { sessionId, cwd, inputs } = await broker.chat(
|
|
83
124
|
chatId,
|
|
84
125
|
args.sessionId,
|
|
85
126
|
args.text,
|
|
86
127
|
context.mcpReq.signal,
|
|
87
128
|
);
|
|
88
|
-
return finishResult(
|
|
89
|
-
|
|
129
|
+
return finishResult(
|
|
130
|
+
broker,
|
|
131
|
+
context,
|
|
132
|
+
textResult({ sessionId, cwd }, inputs),
|
|
133
|
+
);
|
|
134
|
+
}),
|
|
90
135
|
);
|
|
91
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
|
+
|
|
92
272
|
server.registerTool(
|
|
93
273
|
"tools",
|
|
94
274
|
{
|
|
95
275
|
title: "Pi tools",
|
|
96
276
|
description:
|
|
97
|
-
"
|
|
277
|
+
"Get full definitions of Pi tools for call. Filter by names, or omit names to list all active tools.",
|
|
278
|
+
outputSchema,
|
|
98
279
|
inputSchema: z.object({
|
|
99
280
|
names: z
|
|
100
281
|
.array(z.string())
|
|
@@ -106,14 +287,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
106
287
|
.optional()
|
|
107
288
|
.describe("Pi session for this operation only"),
|
|
108
289
|
}),
|
|
109
|
-
annotations:
|
|
110
|
-
readOnlyHint: true,
|
|
111
|
-
destructiveHint: false,
|
|
112
|
-
idempotentHint: true,
|
|
113
|
-
openWorldHint: false,
|
|
114
|
-
},
|
|
290
|
+
annotations: toolAnnotations,
|
|
115
291
|
},
|
|
116
|
-
async (args, context) => {
|
|
292
|
+
handle(async (args, context) => {
|
|
117
293
|
const { inputs, ...inspected } = await broker.tools(
|
|
118
294
|
requireChatId(context),
|
|
119
295
|
args.sessionId,
|
|
@@ -128,7 +304,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
128
304
|
inputs,
|
|
129
305
|
),
|
|
130
306
|
);
|
|
131
|
-
},
|
|
307
|
+
}),
|
|
132
308
|
);
|
|
133
309
|
|
|
134
310
|
server.registerTool(
|
|
@@ -136,7 +312,8 @@ export function createServer(broker: Broker): McpServer {
|
|
|
136
312
|
{
|
|
137
313
|
title: "Call Pi tools",
|
|
138
314
|
description:
|
|
139
|
-
"Execute
|
|
315
|
+
"Execute Pi tools using the definitions returned by tools. Each calls array is one native Pi batch.",
|
|
316
|
+
outputSchema,
|
|
140
317
|
inputSchema: z.object({
|
|
141
318
|
calls: z
|
|
142
319
|
.array(
|
|
@@ -151,12 +328,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
151
328
|
.optional()
|
|
152
329
|
.describe("Pi session for this operation only"),
|
|
153
330
|
}),
|
|
154
|
-
annotations:
|
|
155
|
-
destructiveHint: true,
|
|
156
|
-
openWorldHint: true,
|
|
157
|
-
},
|
|
331
|
+
annotations: toolAnnotations,
|
|
158
332
|
},
|
|
159
|
-
async (args, context) => {
|
|
333
|
+
handle(async (args, context) => {
|
|
160
334
|
const result = await broker.call(
|
|
161
335
|
requireChatId(context),
|
|
162
336
|
args.sessionId,
|
|
@@ -166,9 +340,14 @@ export function createServer(broker: Broker): McpServer {
|
|
|
166
340
|
return finishResult(
|
|
167
341
|
broker,
|
|
168
342
|
context,
|
|
169
|
-
toolResult(
|
|
343
|
+
toolResult(
|
|
344
|
+
result.toolResults,
|
|
345
|
+
result.sessionId,
|
|
346
|
+
result.cwd,
|
|
347
|
+
result.inputs,
|
|
348
|
+
),
|
|
170
349
|
);
|
|
171
|
-
},
|
|
350
|
+
}),
|
|
172
351
|
);
|
|
173
352
|
|
|
174
353
|
for (const tool of directTools) {
|
|
@@ -177,18 +356,14 @@ export function createServer(broker: Broker): McpServer {
|
|
|
177
356
|
{
|
|
178
357
|
title: tool.name,
|
|
179
358
|
description: tool.description,
|
|
359
|
+
outputSchema,
|
|
180
360
|
inputSchema: tool.inputSchema,
|
|
181
|
-
annotations:
|
|
182
|
-
readOnlyHint: tool.name === "read",
|
|
183
|
-
destructiveHint: tool.name !== "read",
|
|
184
|
-
idempotentHint: tool.name === "read",
|
|
185
|
-
openWorldHint: tool.name === "bash" || tool.name === "transfer",
|
|
186
|
-
},
|
|
361
|
+
annotations: toolAnnotations,
|
|
187
362
|
...(tool.fileParams
|
|
188
363
|
? { _meta: { "openai/fileParams": tool.fileParams } }
|
|
189
364
|
: {}),
|
|
190
365
|
},
|
|
191
|
-
async (args, context) => {
|
|
366
|
+
handle(async (args, context) => {
|
|
192
367
|
const input = { ...args } as Record<string, unknown> & {
|
|
193
368
|
sessionId?: string;
|
|
194
369
|
};
|
|
@@ -204,9 +379,14 @@ export function createServer(broker: Broker): McpServer {
|
|
|
204
379
|
return finishResult(
|
|
205
380
|
broker,
|
|
206
381
|
context,
|
|
207
|
-
toolResult(
|
|
382
|
+
toolResult(
|
|
383
|
+
result.toolResults,
|
|
384
|
+
result.sessionId,
|
|
385
|
+
result.cwd,
|
|
386
|
+
result.inputs,
|
|
387
|
+
),
|
|
208
388
|
);
|
|
209
|
-
},
|
|
389
|
+
}),
|
|
210
390
|
);
|
|
211
391
|
}
|
|
212
392
|
|
|
@@ -215,21 +395,17 @@ export function createServer(broker: Broker): McpServer {
|
|
|
215
395
|
{
|
|
216
396
|
title: "Local sessions",
|
|
217
397
|
description:
|
|
218
|
-
"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.",
|
|
399
|
+
outputSchema,
|
|
219
400
|
inputSchema: z.object({
|
|
220
401
|
sessionId: z
|
|
221
402
|
.string()
|
|
222
403
|
.optional()
|
|
223
|
-
.describe("
|
|
404
|
+
.describe("Filter the online list to this Pi session"),
|
|
224
405
|
}),
|
|
225
|
-
annotations:
|
|
226
|
-
readOnlyHint: true,
|
|
227
|
-
destructiveHint: false,
|
|
228
|
-
idempotentHint: true,
|
|
229
|
-
openWorldHint: false,
|
|
230
|
-
},
|
|
406
|
+
annotations: toolAnnotations,
|
|
231
407
|
},
|
|
232
|
-
async (args, context) => {
|
|
408
|
+
handle(async (args, context) => {
|
|
233
409
|
const chatId = requestChatId(context);
|
|
234
410
|
const inputs = chatId
|
|
235
411
|
? await broker.inputs(chatId, args.sessionId, context.mcpReq.signal)
|
|
@@ -241,8 +417,8 @@ export function createServer(broker: Broker): McpServer {
|
|
|
241
417
|
},
|
|
242
418
|
inputs,
|
|
243
419
|
);
|
|
244
|
-
return
|
|
245
|
-
},
|
|
420
|
+
return finishResult(broker, context, result);
|
|
421
|
+
}),
|
|
246
422
|
);
|
|
247
423
|
|
|
248
424
|
server.registerResource(
|
|
@@ -283,19 +459,24 @@ function textResult(
|
|
|
283
459
|
};
|
|
284
460
|
}
|
|
285
461
|
|
|
286
|
-
async function finishResult<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
):
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
...
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
462
|
+
async function finishResult<
|
|
463
|
+
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
464
|
+
>(broker: Broker, context: RequestContext, result: T) {
|
|
465
|
+
const chatId = requestChatId(context);
|
|
466
|
+
const deliveries = chatId ? await broker.deliveries(chatId) : [];
|
|
467
|
+
const answers = chatId ? broker.answers(chatId) : [];
|
|
468
|
+
const content = [
|
|
469
|
+
...result.content,
|
|
470
|
+
...deliveryContent(deliveries),
|
|
471
|
+
...answerContent(answers),
|
|
472
|
+
];
|
|
473
|
+
const structuredContent = {
|
|
474
|
+
text: content
|
|
475
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
476
|
+
.join("\n"),
|
|
477
|
+
};
|
|
478
|
+
await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
|
|
479
|
+
return { ...result, content, structuredContent };
|
|
299
480
|
}
|
|
300
481
|
|
|
301
482
|
function requestChatId(context: RequestContext): string | undefined {
|