Skip to content

Commit d50c93d

Browse files
committed
MCP client connecting to servers in all contexts.
1 parent ec6965d commit d50c93d

16 files changed

Lines changed: 1365 additions & 15 deletions

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,8 @@ docs/
4848
vite.config.ts.timestamp-*.mjs
4949

5050
# Roo Code
51-
.roomodes
51+
.roomodes
52+
53+
# MCP config
54+
packages/api/srcbook_mcp_config.json
55+
MCPClientDevGuide.md

packages/api/ai/generate.mts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PROMPTS_DIR } from '../constants.mjs';
1414
import { encode, decodeCells } from '../srcmd.mjs';
1515
import { buildProjectXml, type FileContent } from '../ai/app-parser.mjs';
1616
import { logAppGeneration } from './logger.mjs';
17+
import { formatMCPToolsForAI } from './mcp-tools.mjs';
1718

1819
const makeGenerateSrcbookSystemPrompt = () => {
1920
return readFileSync(Path.join(PROMPTS_DIR, 'srcbook-generator.txt'), 'utf-8');
@@ -33,25 +34,55 @@ const makeAppEditorSystemPrompt = () => {
3334
return readFileSync(Path.join(PROMPTS_DIR, 'app-editor.txt'), 'utf-8');
3435
};
3536

36-
const makeAppEditorUserPrompt = (projectId: string, files: FileContent[], query: string) => {
37+
const makeAppEditorUserPrompt = async (projectId: string, files: FileContent[], query: string) => {
3738
const projectXml = buildProjectXml(files, projectId);
3839
const userRequestXml = `<userRequest>${query}</userRequest>`;
40+
41+
// Get MCP tools if available
42+
let mcpToolsXml = '';
43+
try {
44+
const mcpTools = await formatMCPToolsForAI();
45+
if (mcpTools && mcpTools !== 'No MCP tools are available.' && mcpTools !== 'Error retrieving MCP tools.') {
46+
mcpToolsXml = `<mcpTools>
47+
${mcpTools}
48+
</mcpTools>`;
49+
}
50+
} catch (error) {
51+
console.error('Error getting MCP tools for app editor:', error);
52+
}
53+
3954
return `Following below are the project XML and the user request.
4055
4156
${projectXml}
4257
4358
${userRequestXml}
59+
${mcpToolsXml ? '\n\n' + mcpToolsXml : ''}
4460
`.trim();
4561
};
4662

47-
const makeAppCreateUserPrompt = (projectId: string, files: FileContent[], query: string) => {
63+
const makeAppCreateUserPrompt = async (projectId: string, files: FileContent[], query: string) => {
4864
const projectXml = buildProjectXml(files, projectId);
4965
const userRequestXml = `<userRequest>${query}</userRequest>`;
66+
67+
// Get MCP tools if available
68+
let mcpToolsXml = '';
69+
try {
70+
const mcpTools = await formatMCPToolsForAI();
71+
if (mcpTools && mcpTools !== 'No MCP tools are available.' && mcpTools !== 'Error retrieving MCP tools.') {
72+
mcpToolsXml = `<mcpTools>
73+
${mcpTools}
74+
</mcpTools>`;
75+
}
76+
} catch (error) {
77+
console.error('Error getting MCP tools for app creation:', error);
78+
}
79+
5080
return `Following below are the project XML and the user request.
5181
5282
${projectXml}
5383
5484
${userRequestXml}
85+
${mcpToolsXml ? '\n\n' + mcpToolsXml : ''}
5586
`.trim();
5687
};
5788

@@ -252,7 +283,7 @@ export async function generateApp(
252283
const result = await generateText({
253284
model,
254285
system: makeAppBuilderSystemPrompt(),
255-
prompt: makeAppCreateUserPrompt(projectId, files, query),
286+
prompt: await makeAppCreateUserPrompt(projectId, files, query),
256287
});
257288
return result.text;
258289
}
@@ -267,7 +298,7 @@ export async function streamEditApp(
267298
const model = await getModel();
268299

269300
const systemPrompt = makeAppEditorSystemPrompt();
270-
const userPrompt = makeAppEditorUserPrompt(projectId, files, query);
301+
const userPrompt = await makeAppEditorUserPrompt(projectId, files, query);
271302

272303
let response = '';
273304

packages/api/ai/mcp-tools.mts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* MCP Tools Formatter
3+
*
4+
* This module provides functions to format MCP tools for AI consumption.
5+
* It converts MCP tool definitions into a format that can be included in AI prompts.
6+
*/
7+
8+
import { getMCPClientManager, type MCPTool } from '../mcp/client-manager.mjs';
9+
10+
/**
11+
* Format MCP tools for inclusion in AI prompts
12+
*
13+
* @returns A formatted string describing all available MCP tools
14+
*/
15+
export async function formatMCPToolsForAI(): Promise<string> {
16+
try {
17+
// Get the MCP client manager
18+
const clientManager = getMCPClientManager();
19+
20+
// Get all available tools
21+
const tools = await clientManager.getTools();
22+
23+
if (tools.length === 0) {
24+
return "No MCP tools are available.";
25+
}
26+
27+
// Format the tools as a string
28+
return formatToolsAsString(tools);
29+
} catch (error) {
30+
console.error('Error formatting MCP tools for AI:', error);
31+
return "Error retrieving MCP tools.";
32+
}
33+
}
34+
35+
/**
36+
* Format a list of MCP tools as a string
37+
*
38+
* @param tools The list of MCP tools to format
39+
* @returns A formatted string describing the tools
40+
*/
41+
function formatToolsAsString(tools: MCPTool[]): string {
42+
// Start with a header
43+
let result = "## Available MCP Tools\n\n";
44+
result += "You can use the following tools to perform actions:\n\n";
45+
46+
// Add each tool
47+
tools.forEach((tool) => {
48+
// Add tool name and description
49+
result += `### ${tool.name}\n`;
50+
if (tool.annotations?.title) {
51+
result += `**${tool.annotations.title}**\n`;
52+
}
53+
if (tool.description) {
54+
result += `${tool.description}\n`;
55+
}
56+
57+
// Add tool annotations as hints
58+
const hints: string[] = [];
59+
if (tool.annotations?.readOnlyHint) hints.push("Read-only");
60+
if (tool.annotations?.destructiveHint) hints.push("Destructive");
61+
if (tool.annotations?.idempotentHint) hints.push("Idempotent");
62+
if (tool.annotations?.openWorldHint) hints.push("Interacts with external systems");
63+
64+
if (hints.length > 0) {
65+
result += `**Hints:** ${hints.join(", ")}\n`;
66+
}
67+
68+
// Add input schema
69+
result += "\n**Input Schema:**\n";
70+
result += "```json\n";
71+
result += JSON.stringify(tool.inputSchema, null, 2);
72+
result += "\n```\n\n";
73+
74+
// Add server ID
75+
result += `**Server:** ${tool.serverId}\n\n`;
76+
77+
// Add separator between tools
78+
result += "---\n\n";
79+
});
80+
81+
// Add usage instructions
82+
result += `## How to Use These Tools
83+
84+
To use a tool, include a tool call in your response using the following format:
85+
86+
\`\`\`
87+
<tool name="TOOL_NAME" server="SERVER_ID">
88+
{
89+
"param1": "value1",
90+
"param2": "value2"
91+
}
92+
</tool>
93+
\`\`\`
94+
95+
Replace TOOL_NAME with the name of the tool you want to use, SERVER_ID with the server ID, and include the appropriate parameters as specified in the tool's input schema.
96+
`;
97+
98+
return result;
99+
}
100+
101+
/**
102+
* Get the list of MCP tools
103+
*
104+
* @returns The list of available MCP tools
105+
*/
106+
export async function getMCPTools(): Promise<MCPTool[]> {
107+
try {
108+
const clientManager = getMCPClientManager();
109+
return await clientManager.getTools();
110+
} catch (error) {
111+
console.error('Error getting MCP tools:', error);
112+
return [];
113+
}
114+
}

packages/api/ai/plan-parser.mts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type App as DBAppType } from '../db/schema.mjs';
44
import { loadFile } from '../apps/disk.mjs';
55
import { StreamingXMLParser, TagType } from './stream-xml-parser.mjs';
66
import { ActionChunkType, DescriptionChunkType } from '@srcbook/shared';
7+
import { getMCPClientManager } from '../mcp/client-manager.mjs';
78

89
// The ai proposes a plan that we expect to contain both files and commands
910
// Here is an example of a plan:
@@ -60,6 +61,15 @@ type NpmInstallCommand = {
6061
description: string;
6162
};
6263

64+
// MCP Tool Action type
65+
interface MCPToolAction {
66+
type: 'tool';
67+
toolName: string;
68+
serverId: string;
69+
parameters: string; // JSON string of parameters
70+
description: string;
71+
}
72+
6373
// Later we can add more commands. For now, we only support npm install
6474
type Command = NpmInstallCommand;
6575

@@ -69,7 +79,7 @@ export interface Plan {
6979
id: string;
7080
query: string;
7181
description: string;
72-
actions: (FileAction | Command)[];
82+
actions: (FileAction | Command | MCPToolAction)[];
7383
}
7484

7585
interface ParsedResult {
@@ -82,13 +92,19 @@ interface ParsedResult {
8292
file?: { '@_filename': string; '#text': string };
8393
commandType?: string;
8494
package?: string | string[];
95+
toolName?: string;
96+
serverId?: string;
97+
parameters?: string;
8598
}[]
8699
| {
87100
'@_type': string;
88101
description: string;
89102
file?: { '@_filename': string; '#text': string };
90103
commandType?: string;
91104
package?: string | string[];
105+
toolName?: string;
106+
serverId?: string;
107+
parameters?: string;
92108
};
93109
};
94110
}
@@ -151,6 +167,30 @@ export async function parsePlan(
151167
packages: Array.isArray(action.package) ? action.package : [action.package],
152168
description: action.description,
153169
});
170+
} else if (action['@_type'] === 'tool' && action.toolName && action.serverId) {
171+
// Handle MCP tool action
172+
try {
173+
// Validate that the tool exists
174+
const clientManager = getMCPClientManager();
175+
const tools = await clientManager.getTools();
176+
const tool = tools.find(t => t.name === action.toolName && t.serverId === action.serverId);
177+
178+
if (!tool) {
179+
console.error(`Tool ${action.toolName} not found on server ${action.serverId}`);
180+
continue;
181+
}
182+
183+
plan.actions.push({
184+
type: 'tool',
185+
toolName: action.toolName,
186+
serverId: action.serverId,
187+
parameters: action.parameters || '{}',
188+
description: action.description,
189+
});
190+
} catch (error) {
191+
console.error('Error handling MCP tool action:', error);
192+
continue;
193+
}
154194
}
155195
}
156196

@@ -274,6 +314,37 @@ async function toStreamingChunk(
274314
packages: packageTags.map((t) => t.content),
275315
},
276316
} as ActionChunkType;
317+
} else if (type === 'tool') {
318+
const toolNameTag = tag.children.find((t) => t.name === 'toolName')!;
319+
const serverIdTag = tag.children.find((t) => t.name === 'serverId')!;
320+
const parametersTag = tag.children.find((t) => t.name === 'parameters');
321+
322+
// Validate that the tool exists
323+
try {
324+
const clientManager = getMCPClientManager();
325+
const tools = await clientManager.getTools();
326+
const tool = tools.find(t => t.name === toolNameTag.content && t.serverId === serverIdTag.content);
327+
328+
if (!tool) {
329+
console.error(`Tool ${toolNameTag.content} not found on server ${serverIdTag.content}`);
330+
return null;
331+
}
332+
333+
return {
334+
type: 'action',
335+
planId: planId,
336+
data: {
337+
type: 'tool',
338+
description,
339+
toolName: toolNameTag.content,
340+
serverId: serverIdTag.content,
341+
parameters: parametersTag ? parametersTag.content : '{}',
342+
},
343+
} as ActionChunkType;
344+
} catch (error) {
345+
console.error('Error handling MCP tool action in streaming parser:', error);
346+
return null;
347+
}
277348
} else {
278349
return null;
279350
}

packages/api/ai/stream-xml-parser.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const xmlSchema: Record<string, NodeSchema> = {
1212
commandType: { isContentNode: true, hasCdata: false },
1313
package: { isContentNode: true, hasCdata: false },
1414
planDescription: { isContentNode: true, hasCdata: true },
15+
toolName: { isContentNode: true, hasCdata: false },
16+
serverId: { isContentNode: true, hasCdata: false },
17+
parameters: { isContentNode: true, hasCdata: true },
1518
};
1619

1720
export type TagType = {

packages/api/apps/disk.mts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FileContent } from '../ai/app-parser.mjs';
1111
import type { Plan } from '../ai/plan-parser.mjs';
1212
import archiver from 'archiver';
1313
import { wss } from '../index.mjs';
14+
import { executeMCPTool } from './mcp-tools.mjs';
1415

1516
export function pathToApp(id: string) {
1617
return Path.join(APPS_DIR, id);
@@ -52,6 +53,23 @@ export async function applyPlan(app: DBAppType, plan: Plan) {
5253
source: item.modified,
5354
binary: isBinary(basename),
5455
});
56+
} else if (item.type === 'tool') {
57+
// Execute MCP tool
58+
try {
59+
console.log(`Executing MCP tool ${item.toolName} on server ${item.serverId}`);
60+
const result = await executeMCPTool(item.toolName, item.serverId, item.parameters);
61+
console.log(`MCP tool execution result:`, result);
62+
63+
// Notify clients about the tool execution
64+
wss.broadcast(`app:${app.externalId}`, 'mcp:tool-executed', {
65+
toolName: item.toolName,
66+
serverId: item.serverId,
67+
result: result
68+
});
69+
} catch (error) {
70+
console.error(`Error executing MCP tool ${item.toolName}:`, error);
71+
// Continue with other actions even if this one fails
72+
}
5573
}
5674
}
5775
} catch (e) {

0 commit comments

Comments
 (0)