Skip to content

Commit e88b011

Browse files
committed
Improve MCP tool handling and OpenAI integration
Refactors MCP tool listing and invocation for better type safety and normalization. Enhances OpenAI chat completion streaming with stricter typing, improved multimodal support, and more robust tool call aggregation. Cleans up type definitions and utility functions for message updates. Fixes minor bugs and improves code clarity throughout MCP and text generation modules.
1 parent 1bcc4e2 commit e88b011

File tree

8 files changed

+237
-154
lines changed

8 files changed

+237
-154
lines changed

src/hooks.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export const init: ServerInit = async () => {
3434
);
3535
}
3636

37-
logger.info("Starting server...");
38-
initExitHandler();
39-
loadMcpServersOnStartup();
37+
logger.info("Starting server...");
38+
initExitHandler();
39+
loadMcpServersOnStartup();
4040

4141
checkAndRunMigrations();
4242
refreshConversationStats();

src/lib/components/chat/ToolUpdate.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
let toolDone = $derived(tool.some(isMessageToolResultUpdate));
2424
let eta = $derived(tool.find((update) => update.subtype === MessageToolUpdateType.ETA)?.eta);
2525
26-
const availableTools: ToolFront[] = (page.data as any)?.tools ?? [];
26+
const toolsData = page.data as { tools?: ToolFront[] } | undefined;
27+
const availableTools: ToolFront[] = toolsData?.tools ?? [];
2728
2829
let loadingBarEl: HTMLDivElement | undefined = $state();
2930
let animation: Animation | undefined = $state();

src/lib/server/mcp/httpClient.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export async function callMcpTool(
3030
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
3131
const url = toUrl(server.url);
3232

33+
const normalizedArgs =
34+
typeof args === "object" && args !== null && !Array.isArray(args)
35+
? (args as Record<string, unknown>)
36+
: undefined;
37+
3338
async function connectStreamable() {
3439
const transport = new StreamableHTTPClientTransport(url, {
3540
requestInit: { headers: server.headers },
@@ -53,19 +58,24 @@ export async function callMcpTool(
5358
usingSse = true;
5459
}
5560

56-
const runTool = async () => client.callTool({ name: tool, arguments: args });
61+
const runTool = async () => client.callTool({ name: tool, arguments: normalizedArgs });
62+
type CallToolResult = Awaited<ReturnType<typeof runTool>>;
5763

5864
const raceWithTimeout = <T>(promise: Promise<T>) =>
5965
Promise.race([
6066
promise,
6167
new Promise<never>((_, reject) => {
62-
controller.signal.addEventListener("abort", () => reject(new Error("MCP tool call timed out")), {
63-
once: true,
64-
});
68+
controller.signal.addEventListener(
69+
"abort",
70+
() => reject(new Error("MCP tool call timed out")),
71+
{
72+
once: true,
73+
}
74+
);
6575
}),
6676
]);
6777

68-
let response: Awaited<ReturnType<typeof runTool>>;
78+
let response: CallToolResult;
6979
try {
7080
response = await raceWithTimeout(runTool());
7181
} catch (error) {
@@ -80,10 +90,14 @@ export async function callMcpTool(
8090
response = await raceWithTimeout(runTool());
8191
}
8292

83-
const parts = Array.isArray(response?.content) ? response.content : [];
93+
const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
8494
const textParts = parts
85-
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
86-
.map((part: any) => part.text as string);
95+
.filter((part): part is { type: "text"; text: string } => {
96+
if (typeof part !== "object" || part === null) return false;
97+
const candidate = part as { type?: unknown; text?: unknown };
98+
return candidate.type === "text" && typeof candidate.text === "string";
99+
})
100+
.map((part) => part.text);
87101

88102
if (textParts.length > 0) {
89103
return textParts.join("\n");

src/lib/server/mcp/registry.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ function parseServers(raw: string): McpServerConfig[] {
2424
const headersRaw = (entry as Record<string, unknown>).headers;
2525
let headers: Record<string, string> | undefined;
2626
if (headersRaw && typeof headersRaw === "object" && !Array.isArray(headersRaw)) {
27-
headers = Object.fromEntries(
28-
Object.entries(headersRaw as Record<string, unknown>).filter(
29-
([, value]): value is string => typeof value === "string"
30-
)
27+
const headerEntries = Object.entries(headersRaw as Record<string, unknown>).filter(
28+
(entry): entry is [string, string] => typeof entry[1] === "string"
3129
);
30+
headers = Object.fromEntries(headerEntries);
3231
}
3332

3433
return headers ? { name, url, headers } : { name, url };

src/lib/server/mcp/tools.ts

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ function sanitizeName(name: string) {
3232
return name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 64);
3333
}
3434

35-
async function listServerTools(server: McpServerConfig) {
35+
type ListedTool = {
36+
name?: string;
37+
inputSchema?: Record<string, unknown>;
38+
description?: string;
39+
annotations?: { title?: string };
40+
};
41+
42+
async function listServerTools(server: McpServerConfig): Promise<ListedTool[]> {
3643
const url = new URL(server.url);
3744
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
3845
try {
@@ -49,7 +56,7 @@ async function listServerTools(server: McpServerConfig) {
4956
}
5057

5158
const response = await client.listTools({});
52-
return Array.isArray(response?.tools) ? response.tools : [];
59+
return Array.isArray(response?.tools) ? (response.tools as ListedTool[]) : [];
5360
} finally {
5461
try {
5562
await client.close?.();
@@ -71,59 +78,64 @@ export async function getOpenAiToolsForMcp(
7178
const tools: OpenAiTool[] = [];
7279
const mapping: Record<string, McpToolMapping> = {};
7380

74-
const seenNames = new Set<string>();
75-
76-
const pushToolDefinition = (
77-
name: string,
78-
description: string | undefined,
79-
parameters: Record<string, unknown> | undefined,
80-
) => {
81-
if (seenNames.has(name)) return;
82-
tools.push({
83-
type: "function",
84-
function: {
85-
name,
86-
description,
87-
parameters,
88-
},
89-
});
90-
seenNames.add(name);
91-
};
92-
93-
for (const server of servers) {
94-
try {
95-
const serverTools = await listServerTools(server);
96-
for (const tool of serverTools as any[]) {
97-
const parameters =
98-
tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : undefined;
99-
const description = tool.description ?? tool?.annotations?.title;
100-
101-
const primaryName = sanitizeName(`${server.name}.${tool.name}`);
102-
pushToolDefinition(primaryName, description, parameters);
103-
mapping[primaryName] = {
104-
fnName: primaryName,
105-
server: server.name,
106-
tool: tool.name,
107-
};
108-
109-
const plainName = sanitizeName(tool.name);
110-
if (!(plainName in mapping)) {
111-
pushToolDefinition(plainName, description, parameters);
112-
mapping[plainName] = {
113-
fnName: plainName,
114-
server: server.name,
115-
tool: tool.name,
116-
};
117-
}
118-
}
119-
} catch {
120-
// Ignore individual server failures
121-
continue;
122-
}
123-
}
124-
125-
cache = { fetchedAt: now, ttlMs, tools, mapping };
126-
return { tools, mapping };
81+
const seenNames = new Set<string>();
82+
83+
const pushToolDefinition = (
84+
name: string,
85+
description: string | undefined,
86+
parameters: Record<string, unknown> | undefined
87+
) => {
88+
if (seenNames.has(name)) return;
89+
tools.push({
90+
type: "function",
91+
function: {
92+
name,
93+
description,
94+
parameters,
95+
},
96+
});
97+
seenNames.add(name);
98+
};
99+
100+
for (const server of servers) {
101+
try {
102+
const serverTools = await listServerTools(server);
103+
for (const tool of serverTools) {
104+
if (typeof tool.name !== "string" || tool.name.trim().length === 0) {
105+
continue;
106+
}
107+
108+
const parameters =
109+
tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : undefined;
110+
const description = tool.description ?? tool.annotations?.title;
111+
const toolName = tool.name;
112+
113+
const primaryName = sanitizeName(`${server.name}.${toolName}`);
114+
pushToolDefinition(primaryName, description, parameters);
115+
mapping[primaryName] = {
116+
fnName: primaryName,
117+
server: server.name,
118+
tool: toolName,
119+
};
120+
121+
const plainName = sanitizeName(toolName);
122+
if (!(plainName in mapping)) {
123+
pushToolDefinition(plainName, description, parameters);
124+
mapping[plainName] = {
125+
fnName: plainName,
126+
server: server.name,
127+
tool: toolName,
128+
};
129+
}
130+
}
131+
} catch {
132+
// Ignore individual server failures
133+
continue;
134+
}
135+
}
136+
137+
cache = { fetchedAt: now, ttlMs, tools, mapping };
138+
return { tools, mapping };
127139
}
128140

129141
export function resetMcpToolsCache() {

0 commit comments

Comments
 (0)