libfx 0.0.7 → 0.0.8-dev.820.g1d9d3b63d6ea
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 +240 -275
- package/browser.js +2 -1
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +658 -208
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +118 -0
- package/node.cjs +2542 -0
- package/node.js +331 -88
- package/package.json +13 -4
- package/skills-node.js +29 -0
- package/skills.js +44 -0
- package/wasm-module.js +50 -0
package/fx-term.wasm
CHANGED
|
Binary file
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
Binary file
|
package/mcp.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const maxTools = 64;
|
|
2
|
+
const maxInstructionsBytes = 64 * 1024;
|
|
3
|
+
|
|
4
|
+
function contentText(content, mode = "tool") {
|
|
5
|
+
if (typeof content === "string") return content;
|
|
6
|
+
const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
|
|
7
|
+
return blocks.map((item) => {
|
|
8
|
+
if (item?.type === "text" && typeof item.text === "string") return item.text;
|
|
9
|
+
if (item?.type === "resource" && typeof item.resource?.text === "string") return item.resource.text;
|
|
10
|
+
if (typeof item?.text === "string") return item.text;
|
|
11
|
+
if (item?.type === "resource_link" && typeof item.uri === "string") return `${item.name ?? "Resource"}: ${item.uri}`;
|
|
12
|
+
if (mode === "instructions" && (item?.type === "image" || item?.type === "audio" || typeof item?.blob === "string" || typeof item?.resource?.blob === "string")) return "[Non-text MCP context is not included in these text instructions.]";
|
|
13
|
+
return "";
|
|
14
|
+
}).filter(Boolean).join("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resultText(result) {
|
|
18
|
+
const text = contentText(result?.content);
|
|
19
|
+
if (result?.structuredContent !== undefined) {
|
|
20
|
+
const structured = JSON.stringify(result.structuredContent);
|
|
21
|
+
return text && text !== structured ? `${text}\n${structured}` : structured;
|
|
22
|
+
}
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function appendInstruction(parts, label, text) {
|
|
27
|
+
if (!text) return;
|
|
28
|
+
parts.push(`<${label}>\n${text}\n</${label}>`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createMcpAdapter(client, options = {}) {
|
|
32
|
+
if (!client || typeof client.listTools !== "function" || typeof client.callTool !== "function") {
|
|
33
|
+
throw new TypeError("MCP client must provide listTools() and callTool()");
|
|
34
|
+
}
|
|
35
|
+
const prefix = options.prefix ?? "";
|
|
36
|
+
if (typeof prefix !== "string" || !/^[A-Za-z0-9_-]*$/.test(prefix)) {
|
|
37
|
+
throw new TypeError("MCP prefix must contain only letters, digits, underscore, or hyphen");
|
|
38
|
+
}
|
|
39
|
+
const catalog = [];
|
|
40
|
+
const cursors = new Set();
|
|
41
|
+
let cursor;
|
|
42
|
+
for (let page = 0; ; page += 1) {
|
|
43
|
+
if (page >= maxTools) throw new RangeError("MCP tool pagination exceeded its page limit");
|
|
44
|
+
const listed = await client.listTools(cursor === undefined ? undefined : { cursor });
|
|
45
|
+
const tools = Array.isArray(listed) ? listed : listed?.tools;
|
|
46
|
+
if (!Array.isArray(tools) || tools.length > maxTools - catalog.length) throw new TypeError("MCP listTools() returned an invalid tool catalog");
|
|
47
|
+
catalog.push(...tools);
|
|
48
|
+
cursor = Array.isArray(listed) ? undefined : listed.nextCursor;
|
|
49
|
+
if (cursor === undefined || cursor === null) break;
|
|
50
|
+
if (typeof cursor !== "string" || cursor.length > 4096 || cursors.has(cursor)) throw new TypeError("MCP listTools() returned an invalid pagination cursor");
|
|
51
|
+
cursors.add(cursor);
|
|
52
|
+
}
|
|
53
|
+
const names = new Set();
|
|
54
|
+
const tools = catalog.map((tool, index) => {
|
|
55
|
+
if (!tool || typeof tool.name !== "string" || tool.name.length === 0 || tool.name.length > 256 || (tool.description !== undefined && typeof tool.description !== "string")) {
|
|
56
|
+
throw new TypeError(`MCP tool ${index} is invalid`);
|
|
57
|
+
}
|
|
58
|
+
if (catalog.some((previous, previousIndex) => previousIndex < index && previous?.name === tool.name)) throw new TypeError(`MCP tool ${tool.name} is duplicated`);
|
|
59
|
+
const base = `${prefix}${tool.name}`.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
60
|
+
let name = base.slice(0, 64);
|
|
61
|
+
for (let suffix = 2; names.has(name); suffix += 1) {
|
|
62
|
+
const tail = `_${suffix}`;
|
|
63
|
+
name = `${base.slice(0, 64 - tail.length)}${tail}`;
|
|
64
|
+
}
|
|
65
|
+
names.add(name);
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
description: tool.description || "MCP tool",
|
|
69
|
+
inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
70
|
+
async execute(input, { signal }) {
|
|
71
|
+
const result = await client.callTool({ name: tool.name, arguments: input }, undefined, { signal });
|
|
72
|
+
const text = resultText(result);
|
|
73
|
+
const images = (Array.isArray(result?.content) ? result.content : []).flatMap((item) => item?.type === "image" ? [item] : item?.type === "resource" && item.resource?.mimeType?.startsWith("image/") && typeof item.resource?.blob === "string" ? [{ type: "image", mimeType: item.resource.mimeType, data: item.resource.blob }] : []).map((item) => ({
|
|
74
|
+
type: "image", mimeType: item.mimeType, data: item.data,
|
|
75
|
+
}));
|
|
76
|
+
const rich = images.length ? { type: "libfx.tool-result", text, images } : null;
|
|
77
|
+
if (result?.isError) {
|
|
78
|
+
const error = new Error(text || `MCP tool ${tool.name} failed`);
|
|
79
|
+
if (rich) error.toolResult = rich;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return rich ?? text;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const instructions = [];
|
|
88
|
+
for (const uri of options.resources ?? []) {
|
|
89
|
+
if (typeof client.readResource !== "function") throw new TypeError("MCP client does not provide readResource()");
|
|
90
|
+
const result = await client.readResource({ uri });
|
|
91
|
+
appendInstruction(instructions, "mcp_resource", contentText(result?.contents ?? result?.content, "instructions"));
|
|
92
|
+
}
|
|
93
|
+
for (const prompt of options.prompts ?? []) {
|
|
94
|
+
if (typeof client.getPrompt !== "function") throw new TypeError("MCP client does not provide getPrompt()");
|
|
95
|
+
const request = typeof prompt === "string" ? { name: prompt } : prompt;
|
|
96
|
+
const result = await client.getPrompt(request);
|
|
97
|
+
appendInstruction(
|
|
98
|
+
instructions,
|
|
99
|
+
"mcp_prompt",
|
|
100
|
+
(result?.messages ?? []).map((message) => contentText(message.content, "instructions")).filter(Boolean).join("\n"),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const instructionText = instructions.join("\n\n");
|
|
104
|
+
if (new TextEncoder().encode(instructionText).length > maxInstructionsBytes) {
|
|
105
|
+
throw new RangeError(`MCP instructions exceed the ${maxInstructionsBytes} byte libfx limit`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let closed = false;
|
|
109
|
+
return {
|
|
110
|
+
tools,
|
|
111
|
+
instructions: instructionText,
|
|
112
|
+
async close() {
|
|
113
|
+
if (closed) return;
|
|
114
|
+
closed = true;
|
|
115
|
+
await client.close?.();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|