From b854b565585fc43ff9435b92bfa31562e8c24e01 Mon Sep 17 00:00:00 2001 From: elpresidank Date: Fri, 10 Apr 2026 05:45:46 -0500 Subject: [PATCH] feat: MCP tool client infrastructure for agent extensibility Add the full MCP tool pipeline enabling agents to invoke external tools (like Brave Search) via MCP servers: - Add ToolRequest/ToolResponse types and mcp-tool topics to @trustgraph/base - Create McpToolService (FlowProcessor) that connects to external MCP servers via @modelcontextprotocol/sdk StreamableHTTP transport - Add createMcpTool() to wire MCP tools into the agent's ReAct loop - Implement config-driven tool registration in AgentService with backward- compatible fallback to hardcoded tools - Add tool filtering by group and state (port of Python tool_filter.py) - Register mcp-tool in gateway dispatcher and export from @trustgraph/flow - Fix flow restart race condition: skip restart when flow definitions unchanged - Update seed config with MCP server config and tool definitions - Add run scripts for MCP tool service and Brave Search MCP server Co-Authored-By: Claude Opus 4.6 (1M context) --- ts/package.json | 1 + .../base/src/processor/flow-processor.ts | 11 + ts/packages/base/src/schema/messages.ts | 13 ++ ts/packages/base/src/schema/topics.ts | 4 + ts/packages/flow/package.json | 1 + ts/packages/flow/src/agent/mcp-tool/index.ts | 1 + .../flow/src/agent/mcp-tool/service.ts | 160 +++++++++++++ ts/packages/flow/src/agent/react/service.ts | 219 ++++++++++++++++-- ts/packages/flow/src/agent/react/tools.ts | 30 ++- ts/packages/flow/src/agent/react/types.ts | 2 + ts/packages/flow/src/agent/tool-filter.ts | 61 +++++ .../flow/src/gateway/dispatch/manager.ts | 1 + ts/packages/flow/src/index.ts | 6 + ts/pnpm-lock.yaml | 3 + ts/scripts/run-brave-mcp.sh | 28 +++ ts/scripts/run-mcp-tool.ts | 18 ++ ts/scripts/seed-config.ts | 58 +++++ 17 files changed, 600 insertions(+), 17 deletions(-) create mode 100644 ts/packages/flow/src/agent/mcp-tool/index.ts create mode 100644 ts/packages/flow/src/agent/mcp-tool/service.ts create mode 100644 ts/packages/flow/src/agent/tool-filter.ts create mode 100755 ts/scripts/run-brave-mcp.sh create mode 100644 ts/scripts/run-mcp-tool.ts diff --git a/ts/package.json b/ts/package.json index 0500f80e..d5154a96 100644 --- a/ts/package.json +++ b/ts/package.json @@ -32,6 +32,7 @@ "document-rag": "tsx scripts/run-document-rag.ts", "create-test-pdf": "tsx scripts/create-test-pdf.ts", "seed:demo": "tsx scripts/seed-demo.ts", + "mcp-tool": "tsx scripts/run-mcp-tool.ts", "llm:azure-openai": "tsx scripts/run-llm-azure-openai.ts", "llm:openai-compat": "tsx scripts/run-llm-openai-compatible.ts", "llm:mistral": "tsx scripts/run-llm-mistral.ts" diff --git a/ts/packages/base/src/processor/flow-processor.ts b/ts/packages/base/src/processor/flow-processor.ts index d732c019..2e8da8b8 100644 --- a/ts/packages/base/src/processor/flow-processor.ts +++ b/ts/packages/base/src/processor/flow-processor.ts @@ -22,6 +22,7 @@ export abstract class FlowProcessor extends AsyncProcessor { private specifications: Spec[] = []; private flows = new Map(); private configConsumer: BackendConsumer | null = null; + private lastFlowsJson = ""; protected constructor(config: ProcessorConfig) { super(config); @@ -76,6 +77,16 @@ export abstract class FlowProcessor extends AsyncProcessor { return; } + // Skip flow restart if the flow definitions haven't changed. + // This prevents disrupting in-flight requests when non-flow config + // sections (prompts, tools, mcp) are updated. + const flowsJson = JSON.stringify(flowDefs); + if (this.lastFlowsJson && flowsJson === this.lastFlowsJson && this.flows.size > 0) { + console.log(`[${this.config.id}] Flow definitions unchanged, skipping restart`); + return; + } + this.lastFlowsJson = flowsJson; + // Stop removed flows for (const [name, flow] of this.flows) { if (!(name in flowDefs)) { diff --git a/ts/packages/base/src/schema/messages.ts b/ts/packages/base/src/schema/messages.ts index 3422b2dd..5710ee37 100644 --- a/ts/packages/base/src/schema/messages.ts +++ b/ts/packages/base/src/schema/messages.ts @@ -304,6 +304,19 @@ export interface CollectionManagementResponse { collections?: { user: string; collection: string; name: string; description: string; tags: string[] }[]; } +// ---------- Tool invocation (MCP tools) ---------- + +export interface ToolRequest { + name: string; + parameters: string; // JSON-encoded +} + +export interface ToolResponse { + error?: TgError; + text?: string; // Plain text response + object?: string; // JSON-encoded structured response +} + // ---------- Flow management ---------- // Flow request/response use kebab-case wire format to match the client. diff --git a/ts/packages/base/src/schema/topics.ts b/ts/packages/base/src/schema/topics.ts index b728367d..62ec6228 100644 --- a/ts/packages/base/src/schema/topics.ts +++ b/ts/packages/base/src/schema/topics.ts @@ -60,6 +60,10 @@ export const topics = { promptRequest: topic("prompt-request"), promptResponse: topic("prompt-response"), + // MCP tool invocation + mcpToolRequest: topic("mcp-tool-request"), + mcpToolResponse: topic("mcp-tool-response"), + // Librarian (document management) librarianRequest: topic("librarian-request"), librarianResponse: topic("librarian-response"), diff --git a/ts/packages/flow/package.json b/ts/packages/flow/package.json index d7a8cd0e..ae849509 100644 --- a/ts/packages/flow/package.json +++ b/ts/packages/flow/package.json @@ -19,6 +19,7 @@ "fastify": "^5.2.0", "ollama": "^0.6.3", "@mistralai/mistralai": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.12.0", "openai": "^4.85.0", "pdfjs-dist": "^5.6.205" }, diff --git a/ts/packages/flow/src/agent/mcp-tool/index.ts b/ts/packages/flow/src/agent/mcp-tool/index.ts new file mode 100644 index 00000000..f2fec7d8 --- /dev/null +++ b/ts/packages/flow/src/agent/mcp-tool/index.ts @@ -0,0 +1 @@ +export { McpToolService } from "./service.js"; diff --git a/ts/packages/flow/src/agent/mcp-tool/service.ts b/ts/packages/flow/src/agent/mcp-tool/service.ts new file mode 100644 index 00000000..1d70d64e --- /dev/null +++ b/ts/packages/flow/src/agent/mcp-tool/service.ts @@ -0,0 +1,160 @@ +/** + * MCP tool-calling service — calls external MCP tool servers. + * + * Receives ToolRequest (name + JSON-encoded parameters) over NATS, + * connects to the appropriate MCP server via the MCP SDK, + * invokes the tool, and returns the result as a ToolResponse. + * + * MCP service configs are pushed via the config system under the "mcp" key. + * + * Python reference: trustgraph-flow/trustgraph/agent/mcp_tool/service.py + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +import { + FlowProcessor, + ConsumerSpec, + ProducerSpec, + type ProcessorConfig, + type FlowContext, + type ToolRequest, + type ToolResponse, +} from "@trustgraph/base"; + +interface McpServiceConfig { + url: string; + "remote-name"?: string; + "auth-token"?: string; +} + +export class McpToolService extends FlowProcessor { + private mcpServices: Record = {}; + + constructor(config: ProcessorConfig) { + super(config); + + this.registerSpecification( + new ConsumerSpec("request", this.onRequest.bind(this)), + ); + this.registerSpecification(new ProducerSpec("response")); + + this.registerConfigHandler(this.onMcpConfig.bind(this)); + } + + private async onMcpConfig( + config: Record, + version: number, + ): Promise { + console.log(`[McpToolService] Got config version ${version}`); + + if (!("mcp" in config) || typeof config.mcp !== "object" || config.mcp === null) { + this.mcpServices = {}; + return; + } + + const mcpConfig = config.mcp as Record; + this.mcpServices = {}; + + for (const [name, value] of Object.entries(mcpConfig)) { + try { + this.mcpServices[name] = JSON.parse(value) as McpServiceConfig; + console.log(`[McpToolService] Registered MCP service: ${name}`); + } catch (err) { + console.error(`[McpToolService] Failed to parse MCP config for ${name}:`, err); + } + } + + console.log( + `[McpToolService] ${Object.keys(this.mcpServices).length} MCP services configured`, + ); + } + + private async onRequest( + msg: ToolRequest, + properties: Record, + flowCtx: FlowContext, + ): Promise { + const requestId = properties.id; + if (!requestId) return; + + const responseProducer = flowCtx.flow.producer("response"); + + try { + const result = await this.invokeTool( + msg.name, + msg.parameters ? JSON.parse(msg.parameters) : {}, + ); + + if (typeof result === "string") { + await responseProducer.send(requestId, { text: result }); + } else { + await responseProducer.send(requestId, { object: JSON.stringify(result) }); + } + } catch (err) { + console.error(`[McpToolService] Error invoking tool ${msg.name}:`, err); + const message = err instanceof Error ? err.message : String(err); + await responseProducer.send(requestId, { + error: { type: "tool-error", message }, + }); + } + } + + private async invokeTool( + name: string, + parameters: Record, + ): Promise { + if (!(name in this.mcpServices)) { + throw new Error(`MCP service "${name}" not known`); + } + + const svcConfig = this.mcpServices[name]; + if (!svcConfig.url) { + throw new Error(`MCP service "${name}" URL not defined`); + } + + const remoteName = svcConfig["remote-name"] ?? name; + + // Build headers with optional bearer token + const headers: Record = {}; + if (svcConfig["auth-token"]) { + headers["Authorization"] = `Bearer ${svcConfig["auth-token"]}`; + } + + console.log(`[McpToolService] Invoking ${remoteName} at ${svcConfig.url}`); + + // Connect to streamable HTTP MCP server + const transport = new StreamableHTTPClientTransport( + new URL(svcConfig.url), + { requestInit: { headers } }, + ); + + const client = new Client({ name: "trustgraph-mcp-client", version: "1.0.0" }); + + try { + await client.connect(transport); + + const result = await client.callTool({ + name: remoteName, + arguments: parameters, + }); + + // Extract response — prefer structured content, fall back to text + if (result.structuredContent) { + return result.structuredContent; + } + + if (result.content && Array.isArray(result.content)) { + return result.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + } + + return "No content"; + } finally { + await transport.close(); + } + } +} diff --git a/ts/packages/flow/src/agent/react/service.ts b/ts/packages/flow/src/agent/react/service.ts index a007446b..80e4f10f 100644 --- a/ts/packages/flow/src/agent/react/service.ts +++ b/ts/packages/flow/src/agent/react/service.ts @@ -9,6 +9,10 @@ * 4. Executes tools and feeds observations back to the LLM * 5. Sends streaming AgentResponse chunks (thought, observation, answer, error) * + * Tools can be registered statically (hardcoded fallback) or dynamically via + * config-push. When a "tool" section is present in config, tools are built + * from that config; otherwise the 3 default tools are used for backward compat. + * * Python reference: trustgraph-flow/trustgraph/agent/react/service.py */ @@ -29,19 +33,26 @@ import { type DocumentRagResponse, type TriplesQueryRequest, type TriplesQueryResponse, + type ToolRequest, + type ToolResponse, } from "@trustgraph/base"; import { createKnowledgeQueryTool, createDocumentQueryTool, createTriplesQueryTool, + createMcpTool, } from "./tools.js"; import { buildReActPrompt } from "./prompt.js"; -import type { AgentTool } from "./types.js"; +import { filterToolsByGroupAndState, getNextState } from "../tool-filter.js"; +import type { AgentTool, ToolArg } from "./types.js"; const MAX_ITERATIONS = 10; export class AgentService extends FlowProcessor { + /** Config-driven tools; null means "use hardcoded fallback". */ + private configuredTools: AgentTool[] | null = null; + constructor(config: ProcessorConfig) { super(config); @@ -83,9 +94,175 @@ export class AgentService extends FlowProcessor { ), ); + // MCP tool invocation client + this.registerSpecification( + new RequestResponseSpec( + "mcp-tool", + "mcp-tool-request", + "mcp-tool-response", + ), + ); + + // Register for config-push to build tools dynamically + this.registerConfigHandler(this.onToolsConfig.bind(this)); + console.log("[AgentService] Service initialized"); } + // ---------- Config-driven tool registration ---------- + + private async onToolsConfig( + config: Record, + version: number, + ): Promise { + console.log(`[AgentService] Loading tool configuration version ${version}`); + + try { + if (!("tool" in config) || typeof config.tool !== "object" || config.tool === null) { + // No tool config — keep using hardcoded fallback + this.configuredTools = null; + console.log("[AgentService] No tool config found, using default tools"); + return; + } + + const toolConfig = config.tool as Record; + const tools: AgentTool[] = []; + + for (const [_toolId, toolValue] of Object.entries(toolConfig)) { + try { + const data = JSON.parse(toolValue) as Record; + const implType = data["type"] as string; + const name = data["name"] as string; + const description = data["description"] as string ?? ""; + + if (!name) { + console.warn(`[AgentService] Skipping tool with no name: ${_toolId}`); + continue; + } + + let tool: AgentTool | null = null; + + switch (implType) { + case "knowledge-query": + // Will be wired to requestor at request time + tool = { + name, + description: description || "Query the knowledge graph for information about entities and their relationships.", + args: [{ name: "question", type: "string", description: "The question to ask" }], + config: data, + execute: async () => "", // placeholder — wired at request time + }; + break; + + case "document-query": + tool = { + name, + description: description || "Search documents for relevant information.", + args: [{ name: "question", type: "string", description: "The question to search for" }], + config: data, + execute: async () => "", + }; + break; + + case "triples-query": + tool = { + name, + description: description || "Query for specific triples in the knowledge graph.", + args: [ + { name: "subject", type: "string", description: "Subject entity (optional)" }, + { name: "predicate", type: "string", description: "Predicate/relationship (optional)" }, + { name: "object", type: "string", description: "Object entity (optional)" }, + ], + config: data, + execute: async () => "", + }; + break; + + case "mcp-tool": { + const configArgs = (data["arguments"] as Array>) ?? []; + const args: ToolArg[] = configArgs.map((a) => ({ + name: a.name ?? "", + type: a.type ?? "string", + description: a.description ?? "", + })); + + // Create a placeholder — will be wired to the MCP requestor at request time + tool = { + name, + description, + args, + config: data, + execute: async () => "", // placeholder + }; + break; + } + + default: + console.warn(`[AgentService] Unknown tool type "${implType}" for ${name}`); + continue; + } + + if (tool) { + tools.push(tool); + console.log(`[AgentService] Registered tool: ${name} (${implType})`); + } + } catch (err) { + console.error(`[AgentService] Failed to parse tool config ${_toolId}:`, err); + } + } + + this.configuredTools = tools.length > 0 ? tools : null; + console.log(`[AgentService] ${tools.length} tools loaded from config`); + } catch (err) { + console.error("[AgentService] Config reload failed:", err); + } + } + + /** + * Wire up tool execute functions with live requestors from the flow context. + * Config-driven tools store placeholders; this replaces them with real impls. + */ + private wireTools(tools: AgentTool[], flowCtx: FlowContext, collection?: string): AgentTool[] { + return tools.map((tool) => { + const implType = tool.config?.["type"] as string | undefined; + + switch (implType) { + case "knowledge-query": { + const live = createKnowledgeQueryTool( + flowCtx.flow.requestor("graph-rag"), + collection, + ); + return { ...tool, execute: live.execute }; + } + case "document-query": { + const live = createDocumentQueryTool( + flowCtx.flow.requestor("doc-rag"), + collection, + ); + return { ...tool, execute: live.execute }; + } + case "triples-query": { + const live = createTriplesQueryTool( + flowCtx.flow.requestor("triples"), + collection, + ); + return { ...tool, execute: live.execute }; + } + case "mcp-tool": { + const live = createMcpTool( + flowCtx.flow.requestor("mcp-tool"), + tool.name, + tool.description, + tool.args, + ); + return { ...tool, execute: live.execute }; + } + default: + return tool; + } + }); + } + private async onRequest( msg: AgentRequest, properties: Record, @@ -97,21 +274,31 @@ export class AgentService extends FlowProcessor { const responseProducer = flowCtx.flow.producer("agent-response"); try { - // Build tools from flow requestors - const tools: AgentTool[] = [ - createKnowledgeQueryTool( - flowCtx.flow.requestor("graph-rag"), - msg.collection, - ), - createDocumentQueryTool( - flowCtx.flow.requestor("doc-rag"), - msg.collection, - ), - createTriplesQueryTool( - flowCtx.flow.requestor("triples"), - msg.collection, - ), - ]; + // Build tools — config-driven or hardcoded fallback + let tools: AgentTool[]; + + if (this.configuredTools) { + tools = this.wireTools(this.configuredTools, flowCtx, msg.collection); + } else { + // Hardcoded fallback (backward compat) + tools = [ + createKnowledgeQueryTool( + flowCtx.flow.requestor("graph-rag"), + msg.collection, + ), + createDocumentQueryTool( + flowCtx.flow.requestor("doc-rag"), + msg.collection, + ), + createTriplesQueryTool( + flowCtx.flow.requestor("triples"), + msg.collection, + ), + ]; + } + + // Apply tool filtering by group and state + tools = filterToolsByGroupAndState(tools, msg.group, msg.state); // Build the ReAct prompt const { system, prompt: initialPrompt } = buildReActPrompt( diff --git a/ts/packages/flow/src/agent/react/tools.ts b/ts/packages/flow/src/agent/react/tools.ts index 7ea78485..d6a229c4 100644 --- a/ts/packages/flow/src/agent/react/tools.ts +++ b/ts/packages/flow/src/agent/react/tools.ts @@ -13,10 +13,12 @@ import type { DocumentRagResponse, TriplesQueryRequest, TriplesQueryResponse, + ToolRequest, + ToolResponse, Term, } from "@trustgraph/base"; -import type { AgentTool } from "./types.js"; +import type { AgentTool, ToolArg } from "./types.js"; /** * Format a Term to a human-readable string. @@ -197,3 +199,29 @@ export function createTriplesQueryTool( }, }; } + +/** + * Create an agent tool that delegates to the MCP tool service via NATS. + * + * The MCP tool service handles the actual MCP server connection; + * this function just wraps it as an AgentTool the ReAct agent can invoke. + */ +export function createMcpTool( + client: RequestResponse, + toolName: string, + description: string, + args: ToolArg[], +): AgentTool { + return { + name: toolName, + description, + args, + async execute(input: string): Promise { + const res = await client.request({ name: toolName, parameters: input }); + if (res.error) return `Error: ${res.error.message}`; + if (res.text) return res.text; + if (res.object) return res.object; + return "No content"; + }, + }; +} diff --git a/ts/packages/flow/src/agent/react/types.ts b/ts/packages/flow/src/agent/react/types.ts index 5b6a3a67..88b29f21 100644 --- a/ts/packages/flow/src/agent/react/types.ts +++ b/ts/packages/flow/src/agent/react/types.ts @@ -13,6 +13,8 @@ export interface AgentTool { description: string; args: ToolArg[]; execute: (input: string) => Promise; + /** Full tool config from config-push (used by tool filtering). */ + config?: Record; } export type ReActState = diff --git a/ts/packages/flow/src/agent/tool-filter.ts b/ts/packages/flow/src/agent/tool-filter.ts new file mode 100644 index 00000000..85b1e31d --- /dev/null +++ b/ts/packages/flow/src/agent/tool-filter.ts @@ -0,0 +1,61 @@ +/** + * Tool filtering logic for the TrustGraph tool group system. + * + * Filters available tools based on group membership and execution state. + * + * Python reference: trustgraph-flow/trustgraph/agent/tool_filter.py + */ + +import type { AgentTool } from "./react/types.js"; + +/** + * Filter tools based on group membership and execution state. + */ +export function filterToolsByGroupAndState( + tools: AgentTool[], + requestedGroups?: string[], + currentState?: string, +): AgentTool[] { + const groups = requestedGroups ?? ["default"]; + const state = currentState || "undefined"; + + return tools.filter((tool) => isToolAvailable(tool, groups, state)); +} + +function isToolAvailable( + tool: AgentTool, + requestedGroups: string[], + currentState: string, +): boolean { + const config = tool.config ?? {}; + + // Get tool groups (default to ["default"]) + let toolGroups = config["group"] as string[] | string | undefined; + if (!toolGroups) toolGroups = ["default"]; + if (!Array.isArray(toolGroups)) toolGroups = [toolGroups]; + + // Get tool applicable states (default to ["*"] = all states) + let applicableStates = config["applicable-states"] as string[] | string | undefined; + if (!applicableStates) applicableStates = ["*"]; + if (!Array.isArray(applicableStates)) applicableStates = [applicableStates]; + + // Group match: wildcard in requested groups, or intersection non-empty + const groupMatch = + requestedGroups.includes("*") || + toolGroups.some((g) => requestedGroups.includes(g)); + + // State match: wildcard in applicable states, or current state matches + const stateMatch = + applicableStates.includes("*") || + applicableStates.includes(currentState); + + return groupMatch && stateMatch; +} + +/** + * Get the next state after successful tool execution. + */ +export function getNextState(tool: AgentTool, currentState: string): string { + const nextState = tool.config?.["state"] as string | undefined; + return nextState || currentState; +} diff --git a/ts/packages/flow/src/gateway/dispatch/manager.ts b/ts/packages/flow/src/gateway/dispatch/manager.ts index 8300da64..ea84ea97 100644 --- a/ts/packages/flow/src/gateway/dispatch/manager.ts +++ b/ts/packages/flow/src/gateway/dispatch/manager.ts @@ -31,6 +31,7 @@ const FLOW_SERVICES: ReadonlyMap ["graph-embeddings", { request: "graph-embeddings-request", response: "graph-embeddings-response" }], ["document-embeddings", { request: "doc-embeddings-request", response: "doc-embeddings-response" }], ["triples", { request: "triples-request", response: "triples-response" }], + ["mcp-tool", { request: "mcp-tool-request", response: "mcp-tool-response" }], ]); /** diff --git a/ts/packages/flow/src/index.ts b/ts/packages/flow/src/index.ts index d23f68a5..f14788ac 100644 --- a/ts/packages/flow/src/index.ts +++ b/ts/packages/flow/src/index.ts @@ -48,6 +48,12 @@ export { ConfigService, type ConfigServiceConfig } from "./config/service.js"; // ReAct agent export { AgentService } from "./agent/react/index.js"; +// MCP tool service +export { McpToolService } from "./agent/mcp-tool/index.js"; + +// Tool filtering +export { filterToolsByGroupAndState, getNextState } from "./agent/tool-filter.js"; + // Librarian service export { LibrarianService, type LibrarianServiceConfig } from "./librarian/service.js"; export { CollectionManager, type CollectionEntry } from "./librarian/collection-manager.js"; diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 7f02474a..f07133e7 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@mistralai/mistralai': specifier: ^1.0.0 version: 1.15.1 + '@modelcontextprotocol/sdk': + specifier: ^1.12.0 + version: 1.29.0(zod@3.25.76) '@qdrant/js-client-rest': specifier: ^1.13.0 version: 1.17.0(typescript@5.9.3) diff --git a/ts/scripts/run-brave-mcp.sh b/ts/scripts/run-brave-mcp.sh new file mode 100755 index 00000000..ba9dd5d8 --- /dev/null +++ b/ts/scripts/run-brave-mcp.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Start the Brave Search MCP server with HTTP transport. +# +# Usage: ./scripts/run-brave-mcp.sh +# +# Requires: +# - @brave/brave-search-mcp-server (npx will auto-install) +# - BRAVE_API_KEY env var or 1Password access + +set -euo pipefail + +# Resolve API key from env or 1Password +if [ -z "${BRAVE_API_KEY:-}" ]; then + if command -v op &>/dev/null; then + BRAVE_API_KEY="$(op read 'op://beep-dev-secrets/beep-ai/BRAVE_API_KEY')" + echo "[brave-mcp] Loaded API key from 1Password" + else + echo "[brave-mcp] ERROR: BRAVE_API_KEY not set and 'op' CLI not found" + exit 1 + fi +fi + +echo "[brave-mcp] Starting Brave Search MCP server on port 8383..." +exec npx --yes @brave/brave-search-mcp-server \ + --brave-api-key "$BRAVE_API_KEY" \ + --transport http \ + --port 8383 \ + --stateless true diff --git a/ts/scripts/run-mcp-tool.ts b/ts/scripts/run-mcp-tool.ts new file mode 100644 index 00000000..48044dda --- /dev/null +++ b/ts/scripts/run-mcp-tool.ts @@ -0,0 +1,18 @@ +/** + * Start the MCP tool service. + * + * Usage: pnpm tsx scripts/run-mcp-tool.ts + * + * Env: + * NATS_URL (default: nats://localhost:4222) + */ +import { McpToolService } from "../packages/flow/src/agent/mcp-tool/index.js"; + +async function run(): Promise { + await McpToolService.launch("mcp-tool"); +} + +run().catch((err) => { + console.error("MCP tool service failed:", err); + process.exit(1); +}); diff --git a/ts/scripts/seed-config.ts b/ts/scripts/seed-config.ts index 685d6fd8..04c483af 100644 --- a/ts/scripts/seed-config.ts +++ b/ts/scripts/seed-config.ts @@ -188,10 +188,68 @@ async function main(): Promise { // Librarian RPC (for PDF decoder) "librarian-request": "tg.flow.librarian-request", "librarian-response": "tg.flow.librarian-response", + // MCP tool invocation + "mcp-tool-request": "tg.flow.mcp-tool-request", + "mcp-tool-response": "tg.flow.mcp-tool-response", }, }, }); + // 3. MCP server configuration (external tool providers) + console.log("\n── MCP Configuration ──"); + const braveApiKey = process.env.BRAVE_API_KEY; + if (braveApiKey) { + await pushConfig(["mcp"], { + "brave-search": JSON.stringify({ + url: "http://localhost:8383/mcp", + "remote-name": "brave_web_search", + }), + }); + console.log(" Brave Search MCP service configured"); + } else { + console.log(" Skipping MCP config (no BRAVE_API_KEY set)"); + } + + // 4. Agent tool configuration (maps tools to implementations) + console.log("\n── Tool Configuration ──"); + const toolConfig: Record = { + "knowledge-query": JSON.stringify({ + type: "knowledge-query", + name: "KnowledgeQuery", + description: "Query the knowledge graph for information about entities and their relationships.", + group: ["default"], + }), + "document-query": JSON.stringify({ + type: "document-query", + name: "DocumentQuery", + description: "Search the document library for relevant information using semantic search.", + group: ["default"], + }), + "triples-query": JSON.stringify({ + type: "triples-query", + name: "TriplesQuery", + description: "Query for specific triples (subject-predicate-object relationships) in the knowledge graph.", + group: ["default"], + }), + }; + + // Add Brave Search tool if API key is available + if (braveApiKey) { + toolConfig["brave-search"] = JSON.stringify({ + type: "mcp-tool", + name: "brave-search", + description: "Search the web using Brave Search. Returns web search results including titles, URLs, and descriptions.", + "mcp-tool": "brave-search", + group: ["default"], + arguments: [ + { name: "query", type: "string", description: "The search query" }, + ], + }); + console.log(" Brave Search tool added"); + } + + await pushConfig(["tool"], toolConfig); + console.log("\nConfiguration seeded successfully."); }