fatfish-world-pi 0.2.0 → 0.2.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/README.md +1 -1
- package/extensions/fatfish-world.ts +65 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 大肥鱼世界 for Pi
|
|
2
2
|
|
|
3
|
-
> Renamed from `fightclub-pi` to **`fatfish-world-pi`** in 0.2.
|
|
3
|
+
> Renamed from `fightclub-pi` to **`fatfish-world-pi`** in 0.2.2.
|
|
4
4
|
|
|
5
5
|
Pi intentionally has no built-in MCP client. This Pi package adds the
|
|
6
6
|
大肥鱼世界 tools as a Pi extension and ships the 大肥鱼世界 Skill.
|
|
@@ -41,7 +41,10 @@ class LocalMcpClient {
|
|
|
41
41
|
const rawArgs = process.env.FATFISH_WORLD_MCP_ARGS;
|
|
42
42
|
if (rawArgs) {
|
|
43
43
|
const parsed = JSON.parse(rawArgs);
|
|
44
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
!Array.isArray(parsed) ||
|
|
46
|
+
parsed.some((value) => typeof value !== "string")
|
|
47
|
+
) {
|
|
45
48
|
throw new Error("FATFISH_WORLD_MCP_ARGS must be a JSON string array");
|
|
46
49
|
}
|
|
47
50
|
this.args = parsed;
|
|
@@ -54,17 +57,26 @@ class LocalMcpClient {
|
|
|
54
57
|
const child = spawn(this.command, this.args, {
|
|
55
58
|
stdio: ["pipe", "pipe", "pipe"],
|
|
56
59
|
env: { ...process.env },
|
|
60
|
+
// Native spawn cannot execute npx.cmd directly on Windows.
|
|
61
|
+
shell: process.platform === "win32",
|
|
57
62
|
});
|
|
58
63
|
this.child = child;
|
|
59
64
|
const lines = createInterface({ input: child.stdout });
|
|
60
65
|
lines.on("line", (line) => this.onLine(line));
|
|
61
|
-
child.stderr.on("data", (chunk) =>
|
|
66
|
+
child.stderr.on("data", (chunk) =>
|
|
67
|
+
process.stderr.write(`[fatfish-world] ${chunk}`),
|
|
68
|
+
);
|
|
62
69
|
child.on("error", (error) => this.failAll(error));
|
|
63
70
|
child.on("exit", (code, signal) => {
|
|
64
|
-
this.failAll(
|
|
71
|
+
this.failAll(
|
|
72
|
+
new Error(`fatfish-world exited (${code ?? signal ?? "unknown"})`),
|
|
73
|
+
);
|
|
65
74
|
});
|
|
66
75
|
await new Promise<void>((resolve, reject) => {
|
|
67
|
-
const timer = setTimeout(
|
|
76
|
+
const timer = setTimeout(
|
|
77
|
+
() => reject(new Error("fatfish-world did not start")),
|
|
78
|
+
15_000,
|
|
79
|
+
);
|
|
68
80
|
child.once("spawn", () => {
|
|
69
81
|
clearTimeout(timer);
|
|
70
82
|
resolve();
|
|
@@ -77,9 +89,13 @@ class LocalMcpClient {
|
|
|
77
89
|
await this.request("initialize", {
|
|
78
90
|
protocolVersion: "2025-06-18",
|
|
79
91
|
capabilities: {},
|
|
80
|
-
clientInfo: { name: "pi-fatfish-world", version: "0.2.
|
|
92
|
+
clientInfo: { name: "pi-fatfish-world", version: "0.2.2" },
|
|
93
|
+
});
|
|
94
|
+
this.write({
|
|
95
|
+
jsonrpc: "2.0",
|
|
96
|
+
method: "notifications/initialized",
|
|
97
|
+
params: {},
|
|
81
98
|
});
|
|
82
|
-
this.write({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
async listTools(): Promise<any[]> {
|
|
@@ -87,9 +103,18 @@ class LocalMcpClient {
|
|
|
87
103
|
return Array.isArray(result?.tools) ? result.tools : [];
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
async callTool(
|
|
106
|
+
async callTool(
|
|
107
|
+
name: string,
|
|
108
|
+
args: Record<string, unknown>,
|
|
109
|
+
signal?: AbortSignal,
|
|
110
|
+
): Promise<any> {
|
|
91
111
|
if (signal?.aborted) throw new Error("tool call aborted");
|
|
92
|
-
return await this.request(
|
|
112
|
+
return await this.request(
|
|
113
|
+
"tools/call",
|
|
114
|
+
{ name, arguments: args },
|
|
115
|
+
60_000,
|
|
116
|
+
signal,
|
|
117
|
+
);
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
private onLine(line: string) {
|
|
@@ -104,16 +129,23 @@ class LocalMcpClient {
|
|
|
104
129
|
if (!pending) return;
|
|
105
130
|
clearTimeout(pending.timer);
|
|
106
131
|
this.pending.delete(message.id);
|
|
107
|
-
if (message.error)
|
|
132
|
+
if (message.error)
|
|
133
|
+
pending.reject(new Error(message.error.message || "MCP error"));
|
|
108
134
|
else pending.resolve(message.result);
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
private write(message: JsonRpc) {
|
|
112
|
-
if (!this.child?.stdin.writable)
|
|
138
|
+
if (!this.child?.stdin.writable)
|
|
139
|
+
throw new Error("fatfish-world is not running");
|
|
113
140
|
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
114
141
|
}
|
|
115
142
|
|
|
116
|
-
private request(
|
|
143
|
+
private request(
|
|
144
|
+
method: string,
|
|
145
|
+
params: unknown,
|
|
146
|
+
timeoutMs = 15_000,
|
|
147
|
+
signal?: AbortSignal,
|
|
148
|
+
): Promise<any> {
|
|
117
149
|
const id = this.nextId++;
|
|
118
150
|
return new Promise((resolve, reject) => {
|
|
119
151
|
const timer = setTimeout(() => {
|
|
@@ -169,11 +201,19 @@ function contentForPi(result: any) {
|
|
|
169
201
|
return [{ type: "text" as const, text: item.text }];
|
|
170
202
|
}
|
|
171
203
|
if (item?.type === "image" && typeof item.data === "string") {
|
|
172
|
-
return [
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
type: "image" as const,
|
|
207
|
+
data: item.data,
|
|
208
|
+
mimeType: item.mimeType ?? "image/png",
|
|
209
|
+
},
|
|
210
|
+
];
|
|
173
211
|
}
|
|
174
212
|
return [{ type: "text" as const, text: JSON.stringify(item) }];
|
|
175
213
|
});
|
|
176
|
-
return mapped.length > 0
|
|
214
|
+
return mapped.length > 0
|
|
215
|
+
? mapped
|
|
216
|
+
: [{ type: "text" as const, text: "大肥鱼世界 returned no content." }];
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
export default async function fatfishWorldPi(pi: any) {
|
|
@@ -189,13 +229,22 @@ export default async function fatfishWorldPi(pi: any) {
|
|
|
189
229
|
label: tool.title || tool.name,
|
|
190
230
|
description: tool.description || `大肥鱼世界 tool: ${tool.name}`,
|
|
191
231
|
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
192
|
-
async execute(
|
|
232
|
+
async execute(
|
|
233
|
+
_toolCallId: string,
|
|
234
|
+
params: Record<string, unknown>,
|
|
235
|
+
signal: AbortSignal,
|
|
236
|
+
) {
|
|
193
237
|
const result = await client.callTool(tool.name, params ?? {}, signal);
|
|
194
238
|
if (result?.isError) {
|
|
195
|
-
const message = contentForPi(result)
|
|
239
|
+
const message = contentForPi(result)
|
|
240
|
+
.map((item: { text?: string }) => item.text ?? "")
|
|
241
|
+
.join("\n");
|
|
196
242
|
throw new Error(message || "大肥鱼世界 tool failed");
|
|
197
243
|
}
|
|
198
|
-
return {
|
|
244
|
+
return {
|
|
245
|
+
content: contentForPi(result),
|
|
246
|
+
details: { tool: tool.name },
|
|
247
|
+
};
|
|
199
248
|
},
|
|
200
249
|
});
|
|
201
250
|
}
|