libfx 0.0.7-dev.632.g9fe8a30c19a7 → 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/mcp.js CHANGED
@@ -1,22 +1,26 @@
1
1
  const maxTools = 64;
2
2
  const maxInstructionsBytes = 64 * 1024;
3
3
 
4
- function contentText(content) {
4
+ function contentText(content, mode = "tool") {
5
5
  if (typeof content === "string") return content;
6
- if (!Array.isArray(content)) return "";
7
- return content.map((item) => {
6
+ const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
7
+ return blocks.map((item) => {
8
8
  if (item?.type === "text" && typeof item.text === "string") return item.text;
9
9
  if (item?.type === "resource" && typeof item.resource?.text === "string") return item.resource.text;
10
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.]";
11
13
  return "";
12
14
  }).filter(Boolean).join("\n");
13
15
  }
14
16
 
15
17
  function resultText(result) {
16
18
  const text = contentText(result?.content);
17
- if (text) return text;
18
- if (result?.structuredContent !== undefined) return JSON.stringify(result.structuredContent);
19
- return "";
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;
20
24
  }
21
25
 
22
26
  function appendInstruction(parts, label, text) {
@@ -32,25 +36,50 @@ export async function createMcpAdapter(client, options = {}) {
32
36
  if (typeof prefix !== "string" || !/^[A-Za-z0-9_-]*$/.test(prefix)) {
33
37
  throw new TypeError("MCP prefix must contain only letters, digits, underscore, or hyphen");
34
38
  }
35
- const listed = await client.listTools();
36
- const catalog = Array.isArray(listed) ? listed : listed?.tools;
37
- if (!Array.isArray(catalog) || catalog.length > maxTools) {
38
- throw new TypeError("MCP listTools() returned an invalid tool catalog");
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);
39
52
  }
53
+ const names = new Set();
40
54
  const tools = catalog.map((tool, index) => {
41
- if (!tool || typeof tool.name !== "string" || typeof tool.description !== "string") {
55
+ if (!tool || typeof tool.name !== "string" || tool.name.length === 0 || tool.name.length > 256 || (tool.description !== undefined && typeof tool.description !== "string")) {
42
56
  throw new TypeError(`MCP tool ${index} is invalid`);
43
57
  }
44
- const name = `${prefix}${tool.name}`;
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);
45
66
  return {
46
67
  name,
47
- description: tool.description,
68
+ description: tool.description || "MCP tool",
48
69
  inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
49
70
  async execute(input, { signal }) {
50
- const result = await client.callTool({ name: tool.name, arguments: input }, { signal });
71
+ const result = await client.callTool({ name: tool.name, arguments: input }, undefined, { signal });
51
72
  const text = resultText(result);
52
- if (result?.isError) throw new Error(text || `MCP tool ${tool.name} failed`);
53
- return text;
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;
54
83
  },
55
84
  };
56
85
  });
@@ -59,7 +88,7 @@ export async function createMcpAdapter(client, options = {}) {
59
88
  for (const uri of options.resources ?? []) {
60
89
  if (typeof client.readResource !== "function") throw new TypeError("MCP client does not provide readResource()");
61
90
  const result = await client.readResource({ uri });
62
- appendInstruction(instructions, "mcp_resource", contentText(result?.contents ?? result?.content));
91
+ appendInstruction(instructions, "mcp_resource", contentText(result?.contents ?? result?.content, "instructions"));
63
92
  }
64
93
  for (const prompt of options.prompts ?? []) {
65
94
  if (typeof client.getPrompt !== "function") throw new TypeError("MCP client does not provide getPrompt()");
@@ -68,7 +97,7 @@ export async function createMcpAdapter(client, options = {}) {
68
97
  appendInstruction(
69
98
  instructions,
70
99
  "mcp_prompt",
71
- (result?.messages ?? []).map((message) => contentText(message.content)).filter(Boolean).join("\n"),
100
+ (result?.messages ?? []).map((message) => contentText(message.content, "instructions")).filter(Boolean).join("\n"),
72
101
  );
73
102
  }
74
103
  const instructionText = instructions.join("\n\n");