From c7f59fd815c6d21fa9e42151f33f0e85431a6ac7 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 17:24:01 +0000 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20remove?= =?UTF-8?q?=20unused=20env=20processing=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sample/genaisrc/mcp.genai.mts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/sample/genaisrc/mcp.genai.mts b/packages/sample/genaisrc/mcp.genai.mts index 83c118932..dbac9d1b2 100644 --- a/packages/sample/genaisrc/mcp.genai.mts +++ b/packages/sample/genaisrc/mcp.genai.mts @@ -17,11 +17,6 @@ async function startMcpServer(name: string, serverConfig: McpServerConfig) { const capabilities = { tools: {} } const { version = "1.0.0", params = [], ...rest } = serverConfig - // fill up empty env with process.env values - for (const ekv of Object.entries(rest.env || {})) { - const [key, value] = ekv - if (value === undefined) rest.env[key] = process.env[key] - } const transport = new StdioClientTransport({ ...rest, stderr: "inherit", From e342ddbb704d2746e07b671e5cc9626d4fa2ab1c Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 18:11:27 +0000 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20MCP=20server=20?= =?UTF-8?q?support=20and=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/chat.ts | 5 + packages/core/src/expander.ts | 4 + packages/core/src/mcp.ts | 106 +++++++++++++----- packages/core/src/promptdom.ts | 25 +++++ packages/core/src/promptrunner.ts | 1 + packages/core/src/runpromptcontext.ts | 33 ++++-- packages/core/src/types/prompt_template.d.ts | 18 ++- packages/core/src/types/prompt_type.d.ts | 6 +- .../sample/genaisrc/mcp-external.genai.mts | 73 ++++++++++++ packages/sample/genaisrc/mcp.genai.mts | 56 +-------- 10 files changed, 235 insertions(+), 92 deletions(-) create mode 100644 packages/sample/genaisrc/mcp-external.genai.mts diff --git a/packages/core/src/chat.ts b/packages/core/src/chat.ts index cfbfcd2ec..c98863c70 100644 --- a/packages/core/src/chat.ts +++ b/packages/core/src/chat.ts @@ -74,6 +74,7 @@ import { serializeLogProb, topLogprobsToMarkdown, } from "./logprob" +import { startMcpServers } from "./mcp" export function toChatCompletionUserMessage( expanded: string, @@ -782,6 +783,7 @@ export async function executeChatSession( prediction: PromptPrediction, completer: ChatCompletionHandler, chatParticipants: ChatParticipant[], + mcpServers: McpServerConfig[], genOptions: GenerationOptions ): Promise { const { @@ -814,6 +816,8 @@ export async function executeChatSession( } ) : undefined + + const servers = await startMcpServers(mcpServers, genOptions) try { trace.startDetails(`🧠 llm chat`) if (toolDefinitions?.length) @@ -923,6 +927,7 @@ export async function executeChatSession( } } } finally { + await servers.dispose() stats.trace(trace) trace.endDetails() } diff --git a/packages/core/src/expander.ts b/packages/core/src/expander.ts index 4adb14df0..6186241e1 100644 --- a/packages/core/src/expander.ts +++ b/packages/core/src/expander.ts @@ -54,6 +54,7 @@ export async function callExpander( let outputProcessors: PromptOutputProcessorHandler[] = [] let chatParticipants: ChatParticipant[] = [] let fileOutputs: FileOutput[] = [] + let mcpServers: McpServerConfig[] = [] let prediction: PromptPrediction let aici: AICIRequest @@ -87,6 +88,7 @@ export async function callExpander( chatParticipants: cps, fileOutputs: fos, prediction: pred, + mcpServers: mcps } = await renderPromptNode(model, node, { flexTokens: options.flexTokens, trace, @@ -99,6 +101,7 @@ export async function callExpander( outputProcessors = ops chatParticipants = cps fileOutputs = fos + mcpServers = mcps prediction = pred if (errors?.length) { for (const error of errors) trace.error(``, error) @@ -136,6 +139,7 @@ export async function callExpander( outputProcessors, chatParticipants, fileOutputs, + mcpServers, prediction, aici, }) diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts index cdbe1121c..f958b7af0 100644 --- a/packages/core/src/mcp.ts +++ b/packages/core/src/mcp.ts @@ -1,35 +1,85 @@ -/* -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { uniqBy } from "es-toolkit" +import { TraceOptions } from "./trace" +import { logError } from "./util" +import { trace } from "console" -interface McpServer { - close(): Promise -} - -const transport = new StdioClientTransport({ - command: "path/to/server", -}); +export async function startMcpServers( + servers: McpServerConfig[], + options?: TraceOptions +): Promise<{ dispose: (options?: TraceOptions) => Promise }> { + servers = uniqBy(servers || [], (s) => s.id) + if (!servers.length) + return { dispose: async (options?: TraceOptions) => {} } -const client = new Client({ - name: "example-client", - version: "1.0.0", -}, { - capabilities: {} -}); + const disposers = await Promise.all( + servers.map((s) => startMcpServer(s.id, s, options)) + ) + return { + dispose: async (options) => { + const { trace } = options || {} + for (const disposer of disposers) { + try { + await disposer.dispose() + } catch (e) { + logError(e) + trace.error(e) + } + } + }, + } +} -await client.connect(transport); +async function startMcpServer( + name: string, + serverConfig: McpServerConfig, + options: TraceOptions +) { + const trace = options.trace.startTraceDetails(`🪚 mcp ${name}`) + try { + const { Client } = await import( + "@modelcontextprotocol/sdk/client/index.js" + ) + const { StdioClientTransport } = await import( + "@modelcontextprotocol/sdk/client/stdio.js" + ) + const capabilities = { tools: {} } + const { version = "1.0.0", params = [], ...rest } = serverConfig + const transport = new StdioClientTransport({ + ...rest, + stderr: "inherit", + }) + const client = new Client({ name, version, params }, { capabilities }) + await client.connect(transport) -// List available resources -const rest = await client.listTools() -for await (const tool of rest.tools) -{ + // list tools + const { tools } = await client.listTools() + for (const tool of tools) { + //console.debug(`mcp: tool ${tool.name}`) + defTool( + `${name}_${tool.name}`, + tool.description, + tool.inputSchema as any, + async (args: any) => { + const { content, ...rest } = args + const res = await client.callTool({ + name: tool.name, + arguments: rest, + }) + return (res.content as { text?: string }[]) + .map((c) => c.text) + .join("\n") + } + ) + } -} -const res = await client.callTool({ - name: "fff", - arguments: { - "ff": null + return { + dispose: async () => { + await client.close() + await transport.close() + }, + } + } finally { + trace.endDetails() } -}) -*/ +} diff --git a/packages/core/src/promptdom.ts b/packages/core/src/promptdom.ts index ab4a84a10..e79e63b00 100644 --- a/packages/core/src/promptdom.ts +++ b/packages/core/src/promptdom.ts @@ -58,6 +58,7 @@ export interface PromptNode extends ContextExpansionOptions { | "chatParticipant" | "fileOutput" | "importTemplate" + | "mcpServer" | undefined children?: PromptNode[] // Child nodes for hierarchical structure error?: unknown // Error information if present @@ -162,6 +163,12 @@ export interface PromptToolNode extends PromptNode { options?: DefToolOptions } +export interface PromptMcpServerNode extends PromptNode { + type: "mcpServer" + config: McpServerConfig + options?: DefToolOptions +} + // Interface for a file merge node. export interface PromptFileMergeNode extends PromptNode { type: "fileMerge" @@ -405,6 +412,13 @@ export function createImportTemplate( return { type: "importTemplate", files, args, options } } +export function createMcpServer( + config: McpServerConfig, + options?: DefToolOptions +): PromptMcpServerNode { + return { type: "mcpServer", config, options } +} + // Function to check if data objects have the same keys and simple values. function haveSameKeysAndSimpleValues(data: object[]): boolean { if (data.length === 0) return true @@ -494,6 +508,7 @@ export interface PromptNodeVisitor { chatParticipant?: (node: PromptChatParticipantNode) => Awaitable // Chat participant node visitor fileOutput?: (node: FileOutputNode) => Awaitable // File output node visitor importTemplate?: (node: PromptImportTemplate) => Awaitable // Import template node visitor + mcpServer?: (node: PromptMcpServerNode) => Awaitable // Mcp server node visitor } // Function to visit nodes in the prompt tree. @@ -539,6 +554,9 @@ export async function visitNode(node: PromptNode, visitor: PromptNodeVisitor) { case "importTemplate": await visitor.importTemplate?.(node as PromptImportTemplate) break + case "mcpServer": + await visitor.mcpServer?.(node as PromptMcpServerNode) + break } if (node.error) visitor.error?.(node) if (!node.error && !node.deleted && node.children) { @@ -562,6 +580,7 @@ export interface PromptNodeRender { messages: ChatCompletionMessageParam[] // Messages for chat completion fileOutputs: FileOutput[] // File outputs prediction: PromptPrediction // predicted output for the prompt + mcpServers: McpServerConfig[] // Mcp servers } /** @@ -1064,6 +1083,7 @@ export async function renderPromptNode( const outputProcessors: PromptOutputProcessorHandler[] = [] const chatParticipants: ChatParticipant[] = [] const fileOutputs: FileOutput[] = [] + const mcpServers: McpServerConfig[] = [] let prediction: PromptPrediction await visitNode(node, { @@ -1187,6 +1207,10 @@ ${trimNewlines(schemaText)} fileOutputs.push(n.output) trace.itemValue(`file output`, n.output.pattern) }, + mcpServer: (n) => { + mcpServers.push(n.config) + trace.itemValue(`mcp server`, n.options.id) + }, }) const res = Object.freeze({ @@ -1200,6 +1224,7 @@ ${trimNewlines(schemaText)} messages, fileOutputs, prediction, + mcpServers, }) return res } diff --git a/packages/core/src/promptrunner.ts b/packages/core/src/promptrunner.ts index a0233030a..535cb64a2 100644 --- a/packages/core/src/promptrunner.ts +++ b/packages/core/src/promptrunner.ts @@ -225,6 +225,7 @@ export async function runTemplate( prediction, completer, chatParticipants, + mcpServers, genOptions ) tracePromptResult(trace, output) diff --git a/packages/core/src/runpromptcontext.ts b/packages/core/src/runpromptcontext.ts index 599b67e0d..45d055e4f 100644 --- a/packages/core/src/runpromptcontext.ts +++ b/packages/core/src/runpromptcontext.ts @@ -21,6 +21,7 @@ import { finalizeMessages, PromptImage, PromptPrediction, + createMcpServer, } from "./promptdom" import { MarkdownTrace } from "./trace" import { GenerationOptions } from "./generation" @@ -45,10 +46,7 @@ import { tracePromptResult, } from "./chat" import { checkCancelled } from "./cancellation" -import { - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, -} from "./chattypes" +import { ChatCompletionMessageParam } from "./chattypes" import { parseModelIdentifier, resolveModelConnectionInfo } from "./models" import { CHAT_REQUEST_PER_MODEL_CONCURRENT_LIMIT, @@ -306,7 +304,8 @@ export function createChatGenerationContext( | string | ToolCallback | AgenticToolCallback - | AgenticToolProviderCallback, + | AgenticToolProviderCallback + | McpServerConfig, description: string | DefToolOptions, parameters?: PromptParametersSchema | JSONSchemaObject, fn?: ChatFunctionHandler, @@ -330,7 +329,10 @@ export function createChatGenerationContext( defOptions ) ) - } else if ((name as ToolCallback | AgenticToolCallback).impl) { + } else if ( + typeof name === "object" && + (name as ToolCallback | AgenticToolCallback).impl + ) { const tool = name as ToolCallback | AgenticToolCallback appendChild( node, @@ -342,7 +344,10 @@ export function createChatGenerationContext( defOptions ) ) - } else if ((name as AgenticToolProviderCallback).functions) { + } else if ( + typeof name === "object" && + (name as AgenticToolProviderCallback).functions + ) { const tools = (name as AgenticToolProviderCallback).functions for (const tool of tools) appendChild( @@ -355,6 +360,14 @@ export function createChatGenerationContext( defOptions ) ) + } else if (typeof name === "object") { + for (const kv of Object.entries(name)) { + const [id, def] = kv + if ((def as McpServerConfig).command) { + const serverConfig = def as McpServerConfig + appendChild(node, createMcpServer(serverConfig, defOptions)) + } + } } } @@ -632,6 +645,7 @@ export function createChatGenerationContext( const fileMerges: FileMergeHandler[] = [] const outputProcessors: PromptOutputProcessorHandler[] = [] const fileOutputs: FileOutput[] = [] + const mcpServers: McpServerConfig[] = [] let prediction: PromptPrediction // expand template @@ -652,6 +666,7 @@ export function createChatGenerationContext( fileOutputs: fos, images: imgs, prediction: pred, + mcpServers: mcp, } = await renderPromptNode(genOptions.model, node, { flexTokens: genOptions.flexTokens, trace: runTrace, @@ -665,6 +680,7 @@ export function createChatGenerationContext( outputProcessors.push(...ops) fileOutputs.push(...fos) images.push(...imgs) + mcpServers.push(...mcp) prediction = pred if (errors?.length) { @@ -710,6 +726,8 @@ export function createChatGenerationContext( chatParticipants.push(...sysr.chatParticipants) if (sysr.fileOutputs?.length) fileOutputs.push(...sysr.fileOutputs) + if (sysr.mcpServers?.length) + mcpServers.push(...sysr.mcpServers) if (sysr.logs?.length) runTrace.details("📝 console.log", sysr.logs) for (const smsg of sysr.messages) { @@ -785,6 +803,7 @@ export function createChatGenerationContext( prediction, completer, chatParticipants, + mcpServers, genOptions ) ) diff --git a/packages/core/src/types/prompt_template.d.ts b/packages/core/src/types/prompt_template.d.ts index 66a061737..3ae5c974a 100644 --- a/packages/core/src/types/prompt_template.d.ts +++ b/packages/core/src/types/prompt_template.d.ts @@ -2492,6 +2492,18 @@ type ChatAgentHandler = ( args: ChatFunctionArgs ) => Awaitable +interface McpServerConfig { + command: string + args: string[] + params?: string[] + version?: string + + id: string + options?: DefToolOptions +} + +type McpServersConfig = Record> + interface ChatGenerationContext extends ChatTurnGenerationContext { defSchema( name: string, @@ -2503,7 +2515,11 @@ interface ChatGenerationContext extends ChatTurnGenerationContext { options?: DefImagesOptions ): void defTool( - tool: ToolCallback | AgenticToolCallback | AgenticToolProviderCallback, + tool: + | ToolCallback + | AgenticToolCallback + | AgenticToolProviderCallback + | McpServersConfig, options?: DefToolOptions ): void defTool( diff --git a/packages/core/src/types/prompt_type.d.ts b/packages/core/src/types/prompt_type.d.ts index cc9d44b14..d632456c3 100644 --- a/packages/core/src/types/prompt_type.d.ts +++ b/packages/core/src/types/prompt_type.d.ts @@ -105,7 +105,11 @@ declare function defFileOutput( * @param fn callback invoked when the LLM requests to run this function */ declare function defTool( - tool: ToolCallback | AgenticToolCallback | AgenticToolProviderCallback, + tool: + | ToolCallback + | AgenticToolCallback + | AgenticToolProviderCallback + | McpServersConfig, options?: DefToolOptions ): void declare function defTool( diff --git a/packages/sample/genaisrc/mcp-external.genai.mts b/packages/sample/genaisrc/mcp-external.genai.mts new file mode 100644 index 000000000..dbac9d1b2 --- /dev/null +++ b/packages/sample/genaisrc/mcp-external.genai.mts @@ -0,0 +1,73 @@ +script({ + description: "Model Context Protocol server demo", +}) + +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js" +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" + +interface McpServerConfig extends Omit { + version?: string + params?: Record +} +type McpServersConfig = Record + +async function startMcpServer(name: string, serverConfig: McpServerConfig) { + console.debug(`mcp: starting '${name}' server`) + + const capabilities = { tools: {} } + const { version = "1.0.0", params = [], ...rest } = serverConfig + const transport = new StdioClientTransport({ + ...rest, + stderr: "inherit", + }) + const client = new Client({ name, version, params }, { capabilities }) + await client.connect(transport) + + // list tools + const { tools } = await client.listTools() + for (const tool of tools) { + //console.debug(`mcp: tool ${tool.name}`) + defTool( + `${name}_${tool.name}`, + tool.description, + tool.inputSchema as any, + async (args: any) => { + const { content, ...rest } = args + const res = await client.callTool({ + name: tool.name, + arguments: rest, + }) + return (res.content as { text?: string }[]) + .map((c) => c.text) + .join("\n") + } + ) + } +} + +async function startMcpServers(config: McpServersConfig) { + await Promise.all( + Object.entries(config).map( + async ([name, serverConfig]) => + await startMcpServer(name, serverConfig) + ) + ) +} + +await startMcpServers({ + memory: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-memory"], + }, + filesystem: { + command: "npx", + args: [ + "-y", + "@modelcontextprotocol/server-filesystem", + path.resolve("."), + ], + }, +}) + +$`Summarize the README.md file at the root of the workspace.` diff --git a/packages/sample/genaisrc/mcp.genai.mts b/packages/sample/genaisrc/mcp.genai.mts index dbac9d1b2..df1e5953e 100644 --- a/packages/sample/genaisrc/mcp.genai.mts +++ b/packages/sample/genaisrc/mcp.genai.mts @@ -2,60 +2,7 @@ script({ description: "Model Context Protocol server demo", }) -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js" -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" - -interface McpServerConfig extends Omit { - version?: string - params?: Record -} -type McpServersConfig = Record - -async function startMcpServer(name: string, serverConfig: McpServerConfig) { - console.debug(`mcp: starting '${name}' server`) - - const capabilities = { tools: {} } - const { version = "1.0.0", params = [], ...rest } = serverConfig - const transport = new StdioClientTransport({ - ...rest, - stderr: "inherit", - }) - const client = new Client({ name, version, params }, { capabilities }) - await client.connect(transport) - - // list tools - const { tools } = await client.listTools() - for (const tool of tools) { - //console.debug(`mcp: tool ${tool.name}`) - defTool( - `${name}_${tool.name}`, - tool.description, - tool.inputSchema as any, - async (args: any) => { - const { content, ...rest } = args - const res = await client.callTool({ - name: tool.name, - arguments: rest, - }) - return (res.content as { text?: string }[]) - .map((c) => c.text) - .join("\n") - } - ) - } -} - -async function startMcpServers(config: McpServersConfig) { - await Promise.all( - Object.entries(config).map( - async ([name, serverConfig]) => - await startMcpServer(name, serverConfig) - ) - ) -} - -await startMcpServers({ +defTool({ memory: { command: "npx", args: ["-y", "@modelcontextprotocol/server-memory"], @@ -69,5 +16,4 @@ await startMcpServers({ ], }, }) - $`Summarize the README.md file at the root of the workspace.` From 7aa4968f67c97ce490a26a7bab50e62a73128101 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 18:42:34 +0000 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20replace?= =?UTF-8?q?=20mcpServers=20with=20disposables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/chat.ts | 7 +-- packages/core/src/dispose.ts | 19 ++++++ packages/core/src/expander.ts | 28 ++++++--- packages/core/src/mcp.ts | 85 ++++++++++----------------- packages/core/src/promptdom.ts | 16 ++++- packages/core/src/promptrunner.ts | 3 +- packages/core/src/runpromptcontext.ts | 12 ++-- 7 files changed, 93 insertions(+), 77 deletions(-) create mode 100644 packages/core/src/dispose.ts diff --git a/packages/core/src/chat.ts b/packages/core/src/chat.ts index c98863c70..c3e5fea26 100644 --- a/packages/core/src/chat.ts +++ b/packages/core/src/chat.ts @@ -3,6 +3,7 @@ import { MarkdownTrace } from "./trace" import { PromptImage, PromptPrediction, renderPromptNode } from "./promptdom" import { LanguageModelConfiguration, host } from "./host" import { GenerationOptions } from "./generation" +import { dispose } from "./dispose" import { JSON5TryParse, JSON5parse, @@ -74,7 +75,6 @@ import { serializeLogProb, topLogprobsToMarkdown, } from "./logprob" -import { startMcpServers } from "./mcp" export function toChatCompletionUserMessage( expanded: string, @@ -783,7 +783,7 @@ export async function executeChatSession( prediction: PromptPrediction, completer: ChatCompletionHandler, chatParticipants: ChatParticipant[], - mcpServers: McpServerConfig[], + disposables: AsyncDisposable[], genOptions: GenerationOptions ): Promise { const { @@ -817,7 +817,6 @@ export async function executeChatSession( ) : undefined - const servers = await startMcpServers(mcpServers, genOptions) try { trace.startDetails(`🧠 llm chat`) if (toolDefinitions?.length) @@ -927,7 +926,7 @@ export async function executeChatSession( } } } finally { - await servers.dispose() + await dispose(disposables, { trace }) stats.trace(trace) trace.endDetails() } diff --git a/packages/core/src/dispose.ts b/packages/core/src/dispose.ts new file mode 100644 index 000000000..a593420c2 --- /dev/null +++ b/packages/core/src/dispose.ts @@ -0,0 +1,19 @@ +import { TraceOptions } from "./trace" +import { arrayify, logError } from "./util" + +export async function dispose( + disposables: ElementOrArray, + options: TraceOptions +) { + const { trace } = options || {} + for (const disposable of arrayify(disposables)) { + if (disposable !== undefined && disposable[Symbol.asyncDispose]) { + try { + await disposable[Symbol.asyncDispose]() + } catch (e) { + logError(e) + trace.error(e) + } + } + } +} diff --git a/packages/core/src/expander.ts b/packages/core/src/expander.ts index 6186241e1..c1a60bd09 100644 --- a/packages/core/src/expander.ts +++ b/packages/core/src/expander.ts @@ -31,6 +31,7 @@ import { GenerationOptions, GenerationStatus } from "./generation" import { AICIRequest, ChatCompletionMessageParam } from "./chattypes" import { promptParametersSchemaToJSONSchema } from "./parameters" import { Project } from "./server/messages" +import { dispose } from "./dispose" export async function callExpander( prj: Project, @@ -54,7 +55,7 @@ export async function callExpander( let outputProcessors: PromptOutputProcessorHandler[] = [] let chatParticipants: ChatParticipant[] = [] let fileOutputs: FileOutput[] = [] - let mcpServers: McpServerConfig[] = [] + let disposables: AsyncDisposable[] = [] let prediction: PromptPrediction let aici: AICIRequest @@ -88,7 +89,7 @@ export async function callExpander( chatParticipants: cps, fileOutputs: fos, prediction: pred, - mcpServers: mcps + disposables: mcps, } = await renderPromptNode(model, node, { flexTokens: options.flexTokens, trace, @@ -101,7 +102,7 @@ export async function callExpander( outputProcessors = ops chatParticipants = cps fileOutputs = fos - mcpServers = mcps + disposables = mcps prediction = pred if (errors?.length) { for (const error of errors) trace.error(``, error) @@ -139,7 +140,7 @@ export async function callExpander( outputProcessors, chatParticipants, fileOutputs, - mcpServers, + disposables, prediction, aici, }) @@ -247,25 +248,30 @@ export async function expandTemplate( const chatParticipants = prompt.chatParticipants.slice(0) const fileOutputs = prompt.fileOutputs.slice(0) const prediction = prompt.prediction + const disposables = prompt.disposables.slice(0) if (prompt.logs?.length) trace.details("📝 console.log", prompt.logs) if (prompt.aici) trace.fence(prompt.aici, "yaml") trace.endDetails() - if (cancellationToken?.isCancellationRequested || status === "cancelled") + if (cancellationToken?.isCancellationRequested || status === "cancelled") { + await dispose(disposables, { trace }) return { status: "cancelled", statusText: "user cancelled", messages, } + } - if (status !== "success" || prompt.messages.length === 0) + if (status !== "success" || prompt.messages.length === 0) { // cancelled + await dispose(disposables, { trace }) return { status, statusText, messages, } + } if (prompt.images?.length) messages.push(toChatCompletionUserMessage("", prompt.images)) @@ -285,12 +291,14 @@ export async function expandTemplate( try { trace.startDetails("👾 systems") for (let i = 0; i < systems.length; ++i) { - if (cancellationToken?.isCancellationRequested) + if (cancellationToken?.isCancellationRequested) { + await dispose(disposables, { trace }) return { status: "cancelled", statusText: "user cancelled", messages, } + } const system = resolveScript(prj, systems[i]) if (!system) @@ -308,6 +316,7 @@ export async function expandTemplate( if (sysr.chatParticipants) chatParticipants.push(...sysr.chatParticipants) if (sysr.fileOutputs) fileOutputs.push(...sysr.fileOutputs) + if (sysr.disposables?.length) disposables.push(...sysr.disposables) if (sysr.logs?.length) trace.details("📝 console.log", sysr.logs) for (const smsg of sysr.messages) { if (smsg.role === "user" && typeof smsg.content === "string") { @@ -326,12 +335,14 @@ export async function expandTemplate( trace.detailsFenced("js", system.jsSource, "js") trace.endDetails() - if (sysr.status !== "success") + if (sysr.status !== "success") { + await dispose(disposables, options) return { status: sysr.status, statusText: sysr.statusText, messages, } + } } } finally { trace.endDetails() @@ -395,5 +406,6 @@ ${schemaTs} fileOutputs, logprobs, topLogprobs, + disposables, } } diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts index f958b7af0..a3af58025 100644 --- a/packages/core/src/mcp.ts +++ b/packages/core/src/mcp.ts @@ -1,39 +1,9 @@ -import { uniqBy } from "es-toolkit" import { TraceOptions } from "./trace" -import { logError } from "./util" -import { trace } from "console" -export async function startMcpServers( - servers: McpServerConfig[], - options?: TraceOptions -): Promise<{ dispose: (options?: TraceOptions) => Promise }> { - servers = uniqBy(servers || [], (s) => s.id) - if (!servers.length) - return { dispose: async (options?: TraceOptions) => {} } - - const disposers = await Promise.all( - servers.map((s) => startMcpServer(s.id, s, options)) - ) - return { - dispose: async (options) => { - const { trace } = options || {} - for (const disposer of disposers) { - try { - await disposer.dispose() - } catch (e) { - logError(e) - trace.error(e) - } - } - }, - } -} - -async function startMcpServer( - name: string, +export async function startMcpServer( serverConfig: McpServerConfig, options: TraceOptions -) { +): Promise<{ tools: ToolCallback[] } & AsyncDisposable> { const trace = options.trace.startTraceDetails(`🪚 mcp ${name}`) try { const { Client } = await import( @@ -44,37 +14,42 @@ async function startMcpServer( ) const capabilities = { tools: {} } - const { version = "1.0.0", params = [], ...rest } = serverConfig + const { id, version = "1.0.0", params = [], ...rest } = serverConfig const transport = new StdioClientTransport({ ...rest, stderr: "inherit", }) - const client = new Client({ name, version, params }, { capabilities }) + const client = new Client( + { name: id, version, params }, + { capabilities } + ) await client.connect(transport) // list tools - const { tools } = await client.listTools() - for (const tool of tools) { - //console.debug(`mcp: tool ${tool.name}`) - defTool( - `${name}_${tool.name}`, - tool.description, - tool.inputSchema as any, - async (args: any) => { - const { content, ...rest } = args - const res = await client.callTool({ - name: tool.name, - arguments: rest, - }) - return (res.content as { text?: string }[]) - .map((c) => c.text) - .join("\n") - } - ) - } - + const { tools: toolDefinitions } = await client.listTools() + const tools = toolDefinitions.map( + ({ name, description, inputSchema }) => + ({ + spec: { + name: `${id}_${name}`, + description, + parameters: inputSchema as any, + }, + impl: async (args: any) => { + const { content, ...rest } = args + const res = await client.callTool({ + name: name, + arguments: rest, + }) + return (res.content as { text?: string }[]) + .map((c) => c.text) + .join("\n") + }, + }) satisfies ToolCallback + ) return { - dispose: async () => { + tools, + [Symbol.asyncDispose]: async () => { await client.close() await transport.close() }, diff --git a/packages/core/src/promptdom.ts b/packages/core/src/promptdom.ts index e79e63b00..cf9723d2f 100644 --- a/packages/core/src/promptdom.ts +++ b/packages/core/src/promptdom.ts @@ -40,6 +40,7 @@ import { promptyParse } from "./prompty" import { jinjaRenderChatMessage } from "./jinja" import { runtimeHost } from "./host" import { hash } from "./crypto" +import { startMcpServer } from "./mcp" // Definition of the PromptNode interface which is an essential part of the code structure. export interface PromptNode extends ContextExpansionOptions { @@ -580,7 +581,7 @@ export interface PromptNodeRender { messages: ChatCompletionMessageParam[] // Messages for chat completion fileOutputs: FileOutput[] // File outputs prediction: PromptPrediction // predicted output for the prompt - mcpServers: McpServerConfig[] // Mcp servers + disposables: AsyncDisposable[] // Disposables } /** @@ -1084,6 +1085,7 @@ export async function renderPromptNode( const chatParticipants: ChatParticipant[] = [] const fileOutputs: FileOutput[] = [] const mcpServers: McpServerConfig[] = [] + const disposables: AsyncDisposable[] = [] let prediction: PromptPrediction await visitNode(node, { @@ -1209,10 +1211,18 @@ ${trimNewlines(schemaText)} }, mcpServer: (n) => { mcpServers.push(n.config) - trace.itemValue(`mcp server`, n.options.id) + trace.itemValue(`mcp server`, n.config.id) }, }) + if (mcpServers.length) { + for (const mcpServer of mcpServers) { + const res = await startMcpServer(mcpServer, options) + tools.push(...res.tools) + disposables.push(res) + } + } + const res = Object.freeze({ images, schemas, @@ -1224,7 +1234,7 @@ ${trimNewlines(schemaText)} messages, fileOutputs, prediction, - mcpServers, + disposables }) return res } diff --git a/packages/core/src/promptrunner.ts b/packages/core/src/promptrunner.ts index 535cb64a2..e9b39b685 100644 --- a/packages/core/src/promptrunner.ts +++ b/packages/core/src/promptrunner.ts @@ -149,6 +149,7 @@ export async function runTemplate( responseSchema, logprobs, topLogprobs, + disposables, } = await expandTemplate( prj, template, @@ -225,7 +226,7 @@ export async function runTemplate( prediction, completer, chatParticipants, - mcpServers, + disposables, genOptions ) tracePromptResult(trace, output) diff --git a/packages/core/src/runpromptcontext.ts b/packages/core/src/runpromptcontext.ts index 45d055e4f..7db3006fd 100644 --- a/packages/core/src/runpromptcontext.ts +++ b/packages/core/src/runpromptcontext.ts @@ -645,7 +645,7 @@ export function createChatGenerationContext( const fileMerges: FileMergeHandler[] = [] const outputProcessors: PromptOutputProcessorHandler[] = [] const fileOutputs: FileOutput[] = [] - const mcpServers: McpServerConfig[] = [] + const disposables: AsyncDisposable[] = [] let prediction: PromptPrediction // expand template @@ -666,7 +666,7 @@ export function createChatGenerationContext( fileOutputs: fos, images: imgs, prediction: pred, - mcpServers: mcp, + disposables: dps, } = await renderPromptNode(genOptions.model, node, { flexTokens: genOptions.flexTokens, trace: runTrace, @@ -680,7 +680,7 @@ export function createChatGenerationContext( outputProcessors.push(...ops) fileOutputs.push(...fos) images.push(...imgs) - mcpServers.push(...mcp) + disposables.push(...dps) prediction = pred if (errors?.length) { @@ -726,8 +726,8 @@ export function createChatGenerationContext( chatParticipants.push(...sysr.chatParticipants) if (sysr.fileOutputs?.length) fileOutputs.push(...sysr.fileOutputs) - if (sysr.mcpServers?.length) - mcpServers.push(...sysr.mcpServers) + if (sysr.disposables?.length) + disposables.push(...sysr.disposables) if (sysr.logs?.length) runTrace.details("📝 console.log", sysr.logs) for (const smsg of sysr.messages) { @@ -803,7 +803,7 @@ export function createChatGenerationContext( prediction, completer, chatParticipants, - mcpServers, + disposables, genOptions ) ) From fafed982ba1fe96c685d0426d5efe43178bb1ae9 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 18:49:42 +0000 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20enhance=20MCP=20server=20hand?= =?UTF-8?q?ling=20and=20logging=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/mcp.ts | 7 +++++-- packages/core/src/promptdom.ts | 18 +++++++++++++----- packages/core/src/runpromptcontext.ts | 7 +++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts index a3af58025..5a0b47449 100644 --- a/packages/core/src/mcp.ts +++ b/packages/core/src/mcp.ts @@ -1,10 +1,13 @@ import { TraceOptions } from "./trace" +import { logVerbose } from "./util" export async function startMcpServer( serverConfig: McpServerConfig, options: TraceOptions ): Promise<{ tools: ToolCallback[] } & AsyncDisposable> { - const trace = options.trace.startTraceDetails(`🪚 mcp ${name}`) + const { id, version = "1.0.0", params = [], ...rest } = serverConfig + logVerbose(`mcp: starting ${id}`) + const trace = options.trace.startTraceDetails(`🪚 mcp ${id}`) try { const { Client } = await import( "@modelcontextprotocol/sdk/client/index.js" @@ -14,7 +17,6 @@ export async function startMcpServer( ) const capabilities = { tools: {} } - const { id, version = "1.0.0", params = [], ...rest } = serverConfig const transport = new StdioClientTransport({ ...rest, stderr: "inherit", @@ -50,6 +52,7 @@ export async function startMcpServer( return { tools, [Symbol.asyncDispose]: async () => { + logVerbose(`mcp: closing ${id}`) await client.close() await transport.close() }, diff --git a/packages/core/src/promptdom.ts b/packages/core/src/promptdom.ts index cf9723d2f..65157f9d8 100644 --- a/packages/core/src/promptdom.ts +++ b/packages/core/src/promptdom.ts @@ -167,7 +167,6 @@ export interface PromptToolNode extends PromptNode { export interface PromptMcpServerNode extends PromptNode { type: "mcpServer" config: McpServerConfig - options?: DefToolOptions } // Interface for a file merge node. @@ -400,7 +399,7 @@ export function createChatParticipant( // Function to create a file output node. export function createFileOutput(output: FileOutput): FileOutputNode { - return { type: "fileOutput", output } + return { type: "fileOutput", output } satisfies FileOutputNode } // Function to create an import template node. @@ -410,14 +409,23 @@ export function createImportTemplate( options?: ImportTemplateOptions ): PromptImportTemplate { assert(!!files) - return { type: "importTemplate", files, args, options } + return { + type: "importTemplate", + files, + args, + options, + } satisfies PromptImportTemplate } export function createMcpServer( + id: string, config: McpServerConfig, options?: DefToolOptions ): PromptMcpServerNode { - return { type: "mcpServer", config, options } + return { + type: "mcpServer", + config: { ...config, id, options }, + } satisfies PromptMcpServerNode } // Function to check if data objects have the same keys and simple values. @@ -1234,7 +1242,7 @@ ${trimNewlines(schemaText)} messages, fileOutputs, prediction, - disposables + disposables, }) return res } diff --git a/packages/core/src/runpromptcontext.ts b/packages/core/src/runpromptcontext.ts index 7db3006fd..a2e55a215 100644 --- a/packages/core/src/runpromptcontext.ts +++ b/packages/core/src/runpromptcontext.ts @@ -305,7 +305,7 @@ export function createChatGenerationContext( | ToolCallback | AgenticToolCallback | AgenticToolProviderCallback - | McpServerConfig, + | McpServersConfig, description: string | DefToolOptions, parameters?: PromptParametersSchema | JSONSchemaObject, fn?: ChatFunctionHandler, @@ -365,7 +365,10 @@ export function createChatGenerationContext( const [id, def] = kv if ((def as McpServerConfig).command) { const serverConfig = def as McpServerConfig - appendChild(node, createMcpServer(serverConfig, defOptions)) + appendChild( + node, + createMcpServer(id, serverConfig, defOptions) + ) } } } From 6c63fe79a1d5ab402b8b7d84cd369385793c4187 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:19:20 +0000 Subject: [PATCH 05/10] adding docs --- docs/src/assets/mcp.png | Bin 0 -> 45982 bytes docs/src/assets/mcp.png.txt | 1 + .../src/content/docs/guides/agentic-tools.mdx | 2 +- .../docs/reference/scripts/mcp-tools.mdx | 47 ++++++++++++++++ .../content/docs/reference/scripts/tools.mdx | 52 ++++++++++++++---- 5 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 docs/src/assets/mcp.png create mode 100644 docs/src/assets/mcp.png.txt create mode 100644 docs/src/content/docs/reference/scripts/mcp-tools.mdx diff --git a/docs/src/assets/mcp.png b/docs/src/assets/mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..b337d050c7f982e40637c166bc6960717f83e259 GIT binary patch literal 45982 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 g|8D^IzcW$Bn3L + +[Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an emerging standard +for portable tool definitions. + +The MCP defines a protocol that allows to share [tools](https://modelcontextprotocol.io/docs/concepts/tools) +and consume them reguardless of the underlying framework/runtime. + +**GenAIScript implements a client for MCP tools**. + +## Configuring servers + +You can use [defTool](/genaiscript/reference/scripts/tools) to declare a set of server configurations, +using the same syntax as in the [Claude configuration file](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#using-an-mcp-client). + +```js +defTool({ + memory: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-memory"], + }, + filesystem: { + command: "npx", + args: [ + "-y", + "@modelcontextprotocol/server-filesystem", + path.resolve("."), + ], + }, +}) +``` + +GenAIScript will launch the server and register all the tools listed by the server. +The tool identifier will be `server_toolname` to avoid clashes. + +## Finding servers + +The list of available servers can be found in the [Model Context Protocol Servers project](https://github.com/modelcontextprotocol/servers). \ No newline at end of file diff --git a/docs/src/content/docs/reference/scripts/tools.mdx b/docs/src/content/docs/reference/scripts/tools.mdx index 9f2162021..b681455da 100644 --- a/docs/src/content/docs/reference/scripts/tools.mdx +++ b/docs/src/content/docs/reference/scripts/tools.mdx @@ -72,7 +72,39 @@ to evaluate a math expression. title="math-agent.genai.mjs" /> -## Fallback Tools +## Agentic Tools + +[Agentic](https://agentic.so) is +a standard library of AI functions / tools +which are optimized for both normal TS-usage as well as LLM-based usage. +You can register any agentic tool in your script using `defTool`. + +```js +import { calculator } from "@agentic/calculator" +defTool(calculator) +``` + +See [Agentic tools](/genaiscript/reference/scripts/agentic-tools) for more information. + +## Model Context Protocol Tools + +[Model Context Provider](https://modelcontextprotocol.io/) (MCP) is an open protocol +that enables seamless integration between LLM applications and external data sources and [tools](https://modelcontextprotocol.io/docs/concepts/tools). + +You can leverage [MCP servers](https://github.com/modelcontextprotocol/servers) to provide tools to your LLM. + +```js +defTool({ + memory: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-memory"], + }, +}) +``` + +See [Model Context Protocol Tools](/genaiscript/reference/scripts/mcp-tools) for more information. + +## Fallback Tool Support Some LLM models do not have built-in model support. For those model, it is possible to enable tool support through system prompts. The performance may be lower than built-in tools, but it is still possible to use tools. @@ -86,7 +118,7 @@ tools so it will happen automatically for those models. To enable this mode, you can either -- add the `fallbackTools` option to the script +- add the `fallbackTools` option to the script ```js "fallbackTools: true" script({ @@ -94,7 +126,7 @@ script({ }) ``` -- or add the `--fallack-tools` flag to the CLI +- or add the `--fallack-tools` flag to the CLI ```sh "--fallback-tools" npx genaiscript run ... --fallback-tools @@ -119,12 +151,12 @@ script({ defTool("current_weather", ...) ``` -then use the script id in the `tools` field. +then use the tool id in the `tools` field. -```js 'tools: ["system.current_weather"]' +```js 'tools: ["current_weather"]' script({ ..., - tools: ["system.current_weather"], + tools: ["current_weather"], }) ``` @@ -132,13 +164,13 @@ script({ Let's illustrate how tools come together with a question answering script. -In the script below, we add the `system.retrieval_web_search` which registers the `retrieval_web_search` tool. This tool +In the script below, we add the `retrieval_web_search` tool. This tool will call into `retrieval.webSearch` as needed. ```js file="answers.genai.mjs" script({ title: "Answer questions", - system: ["system", "system.retrieval_web_search"] + tool: ["retrieval_web_search"] }) def("FILES", env.files) @@ -151,8 +183,8 @@ $`Answer the questions in FILES using a web search. We can then apply this script to the `questions.md` file below. ```md file="questions.md" -- What is the weather in Seattle? -- What laws were voted in the USA congress last week? +- What is the weather in Seattle? +- What laws were voted in the USA congress last week? ``` After the first request, the LLM requests to call the `web_search` for each questions. From 0d493305600863fa58d5dccaae8e4f905786bd3d Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:21:57 +0000 Subject: [PATCH 06/10] =?UTF-8?q?docs:=20=F0=9F=93=9D=20relocate=20Agentic?= =?UTF-8?q?=20Tools=20section=20in=20tools.mdx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/docs/reference/scripts/tools.mdx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/reference/scripts/tools.mdx b/docs/src/content/docs/reference/scripts/tools.mdx index b681455da..4b2f28505 100644 --- a/docs/src/content/docs/reference/scripts/tools.mdx +++ b/docs/src/content/docs/reference/scripts/tools.mdx @@ -72,20 +72,6 @@ to evaluate a math expression. title="math-agent.genai.mjs" /> -## Agentic Tools - -[Agentic](https://agentic.so) is -a standard library of AI functions / tools -which are optimized for both normal TS-usage as well as LLM-based usage. -You can register any agentic tool in your script using `defTool`. - -```js -import { calculator } from "@agentic/calculator" -defTool(calculator) -``` - -See [Agentic tools](/genaiscript/reference/scripts/agentic-tools) for more information. - ## Model Context Protocol Tools [Model Context Provider](https://modelcontextprotocol.io/) (MCP) is an open protocol @@ -104,6 +90,21 @@ defTool({ See [Model Context Protocol Tools](/genaiscript/reference/scripts/mcp-tools) for more information. + +## Agentic Tools + +[Agentic](https://agentic.so) is +a standard library of AI functions / tools +which are optimized for both normal TS-usage as well as LLM-based usage. +You can register any agentic tool in your script using `defTool`. + +```js +import { calculator } from "@agentic/calculator" +defTool(calculator) +``` + +See [Agentic tools](/genaiscript/guides/agentic-tools) for more information. + ## Fallback Tool Support Some LLM models do not have built-in model support. From f97d061858af8c5bc242b578fe8afeb7d12e3599 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:29:33 +0000 Subject: [PATCH 07/10] =?UTF-8?q?docs:=20update=20LLM=20Tools=20section=20?= =?UTF-8?q?and=20add=20MCP=20examples=20=E2=9C=8F=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++++++------- docs/src/content/docs/index.mdx | 32 ++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2ed7e1443..bf996dbbf 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ 🚀 **JavaScript-ish environment with convenient tooling for file ingestion, prompt development, and structured data extraction.** -- 📄 **Read the ONLINE DOCUMENTATION at [microsoft.github.io/genaiscript](https://microsoft.github.io/genaiscript/)** -- 📺 Watch an [interview on YouTube with nickyt](https://www.youtube.com/watch?v=aeXQ2MJ0Ye0) -- 🎙️ **Listen to the (cringy) podcast** (generated by NotebookLM). +- 📄 **Read the ONLINE DOCUMENTATION at [microsoft.github.io/genaiscript](https://microsoft.github.io/genaiscript/)** +- 📺 Watch an [interview on YouTube with nickyt](https://www.youtube.com/watch?v=aeXQ2MJ0Ye0) +- 🎙️ **Listen to the (cringy) podcast** (generated by NotebookLM). https://github.com/user-attachments/assets/ce181cc0-47d5-41cd-bc03-f220407d4dd0 @@ -18,9 +18,9 @@ https://github.com/user-attachments/assets/ce181cc0-47d5-41cd-bc03-f220407d4dd0 Programmatically assemble prompts for LLMs using JavaScript. Orchestrate LLMs, tools, and data in a single script. -- JavaScript toolbox to work with prompts -- Abstraction to make it easy and productive -- Seamless Visual Studio Code integration +- JavaScript toolbox to work with prompts +- Abstraction to make it easy and productive +- Seamless Visual Studio Code integration ## Hello world @@ -150,7 +150,7 @@ const { files } = await workspace.grep(/[a-z][a-z0-9]+/, { globs: "*.md" }) ### LLM Tools Register JavaScript functions as [tools](https://microsoft.github.io/genaiscript/reference/scripts/tools) -(with fallback for models that don't support tools). +(with fallback for models that don't support tools). [Model Context Protocol (MCP) tools](https://microsoft.github.io/genaiscript/reference/scripts/mcp-tools) are also supported. ```js defTool( diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 61ed56159..4a162007e 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -38,9 +38,9 @@ import testExplorerAlt from "../../assets/vscode-test-explorer.png.txt?raw" Programmatically assemble prompts for LLMs using JavaScript. Orchestrate LLMs, tools, and data in a single script. -- JavaScript toolbox to work with prompts -- Abstraction to make it easy and productive -- Seamless Visual Studio Code integration +- JavaScript toolbox to work with prompts +- Abstraction to make it easy and productive +- Seamless Visual Studio Code integration ## Hello world @@ -136,13 +136,25 @@ defTool("weather", "live weather", { ... "sunny" } ) ``` - -or use built-in [@agentic tools](/genaiscript/guides/agentic-tools/) +or use [@agentic tools](/genaiscript/guides/agentic-tools/) ```js wrap import { WeatherClient } from "@agentic/weather" defTool(new WeatherClient()) -``` +```` + + + + + +Use [tools](https://modelcontextprotocol.io/docs/concepts/tools) exposed in [MCP Servers](/genaiscript/reference/scripts/mcp-tools) + +````js wrap +defTool({ "memory": { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-memory"] +}}) +```` @@ -174,9 +186,9 @@ Scripts are [files](/genaiscript/reference/scripts/)! They can be versioned, sha -- genaisrc - - my-script.genai.mjs - - another-great-script.genai.mjs +- genaisrc + - my-script.genai.mjs + - another-great-script.genai.mjs @@ -250,7 +262,7 @@ The quick brown fox jumps over the lazy dog. -- poem.txt extracted by genaiscript +- poem.txt extracted by genaiscript From 74f11e8be9eb9d703403c9eb15854c504b446818 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:37:54 +0000 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=F0=9F=8E=89=20enhance=20content?= =?UTF-8?q?=20handling=20in=20startMcpServer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/mcp.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts index 5a0b47449..25753f5d1 100644 --- a/packages/core/src/mcp.ts +++ b/packages/core/src/mcp.ts @@ -1,5 +1,11 @@ import { TraceOptions } from "./trace" -import { logVerbose } from "./util" +import { arrayify, logVerbose } from "./util" +import type { + TextContent, + ImageContent, + EmbeddedResource, +} from "@modelcontextprotocol/sdk/types.js" +import { YAMLStringify } from "./yaml" export async function startMcpServer( serverConfig: McpServerConfig, @@ -38,14 +44,32 @@ export async function startMcpServer( parameters: inputSchema as any, }, impl: async (args: any) => { - const { content, ...rest } = args + const { context, ...rest } = args const res = await client.callTool({ name: name, arguments: rest, }) - return (res.content as { text?: string }[]) - .map((c) => c.text) + const content = res.content as ( + | TextContent + | ImageContent + | EmbeddedResource + )[] + let text = arrayify(content) + ?.map((c) => { + switch (c.type) { + case "text": + return c.text || "" + case "image": + return c.data + case "resource": + return c.resource?.uri || "" + default: + return c + } + }) .join("\n") + if (res.isError) text = `Tool Error\n${text}` + return text }, }) satisfies ToolCallback ) From be5172d9afc2b580b9df3e47d7ee20264aa87e4b Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:38:54 +0000 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20add=20model=20and=20tests=20to=20?= =?UTF-8?q?script=20configuration=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sample/genaisrc/mcp.genai.mts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sample/genaisrc/mcp.genai.mts b/packages/sample/genaisrc/mcp.genai.mts index df1e5953e..cb34d3967 100644 --- a/packages/sample/genaisrc/mcp.genai.mts +++ b/packages/sample/genaisrc/mcp.genai.mts @@ -1,5 +1,7 @@ script({ description: "Model Context Protocol server demo", + model: "small", + tests: {}, }) defTool({ From cc079513f5d06d76544cbc9694c8587345a2bd86 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Dec 2024 19:43:43 +0000 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20server=20lifecy?= =?UTF-8?q?cle=20and=20poem=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/docs/reference/scripts/mcp-tools.mdx | 7 +++++++ packages/sample/genaisrc/mcp.genai.mts | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/docs/src/content/docs/reference/scripts/mcp-tools.mdx b/docs/src/content/docs/reference/scripts/mcp-tools.mdx index 6808f9f90..e47fe97a9 100644 --- a/docs/src/content/docs/reference/scripts/mcp-tools.mdx +++ b/docs/src/content/docs/reference/scripts/mcp-tools.mdx @@ -42,6 +42,13 @@ defTool({ GenAIScript will launch the server and register all the tools listed by the server. The tool identifier will be `server_toolname` to avoid clashes. +## Lifecycle of servers + +Servers are started when rendering the prompt and stopped once the chat session is completed. + +This means that if you define servers in an [inline prompt](/genaiscript/reference/prompts/inline), +the server will be started/stopped for each inline prompt. + ## Finding servers The list of available servers can be found in the [Model Context Protocol Servers project](https://github.com/modelcontextprotocol/servers). \ No newline at end of file diff --git a/packages/sample/genaisrc/mcp.genai.mts b/packages/sample/genaisrc/mcp.genai.mts index cb34d3967..d2bb1994b 100644 --- a/packages/sample/genaisrc/mcp.genai.mts +++ b/packages/sample/genaisrc/mcp.genai.mts @@ -4,6 +4,20 @@ script({ tests: {}, }) +await runPrompt((ctx) => { + ctx.defTool({ + filesystem: { + command: "npx", + args: [ + "-y", + "@modelcontextprotocol/server-filesystem", + path.resolve("."), + ], + }, + }) + ctx.$`Write a poem about the file README.md` +}) + defTool({ memory: { command: "npx",