mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-16 08:41:03 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
f2b376abef
commit
b854b56558
17 changed files with 600 additions and 17 deletions
|
|
@ -32,6 +32,7 @@
|
||||||
"document-rag": "tsx scripts/run-document-rag.ts",
|
"document-rag": "tsx scripts/run-document-rag.ts",
|
||||||
"create-test-pdf": "tsx scripts/create-test-pdf.ts",
|
"create-test-pdf": "tsx scripts/create-test-pdf.ts",
|
||||||
"seed:demo": "tsx scripts/seed-demo.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:azure-openai": "tsx scripts/run-llm-azure-openai.ts",
|
||||||
"llm:openai-compat": "tsx scripts/run-llm-openai-compatible.ts",
|
"llm:openai-compat": "tsx scripts/run-llm-openai-compatible.ts",
|
||||||
"llm:mistral": "tsx scripts/run-llm-mistral.ts"
|
"llm:mistral": "tsx scripts/run-llm-mistral.ts"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export abstract class FlowProcessor extends AsyncProcessor {
|
||||||
private specifications: Spec[] = [];
|
private specifications: Spec[] = [];
|
||||||
private flows = new Map<string, Flow>();
|
private flows = new Map<string, Flow>();
|
||||||
private configConsumer: BackendConsumer<ConfigPush> | null = null;
|
private configConsumer: BackendConsumer<ConfigPush> | null = null;
|
||||||
|
private lastFlowsJson = "";
|
||||||
|
|
||||||
protected constructor(config: ProcessorConfig) {
|
protected constructor(config: ProcessorConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
@ -76,6 +77,16 @@ export abstract class FlowProcessor extends AsyncProcessor {
|
||||||
return;
|
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
|
// Stop removed flows
|
||||||
for (const [name, flow] of this.flows) {
|
for (const [name, flow] of this.flows) {
|
||||||
if (!(name in flowDefs)) {
|
if (!(name in flowDefs)) {
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,19 @@ export interface CollectionManagementResponse {
|
||||||
collections?: { user: string; collection: string; name: string; description: string; tags: string[] }[];
|
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 management ----------
|
||||||
|
|
||||||
// Flow request/response use kebab-case wire format to match the client.
|
// Flow request/response use kebab-case wire format to match the client.
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ export const topics = {
|
||||||
promptRequest: topic("prompt-request"),
|
promptRequest: topic("prompt-request"),
|
||||||
promptResponse: topic("prompt-response"),
|
promptResponse: topic("prompt-response"),
|
||||||
|
|
||||||
|
// MCP tool invocation
|
||||||
|
mcpToolRequest: topic("mcp-tool-request"),
|
||||||
|
mcpToolResponse: topic("mcp-tool-response"),
|
||||||
|
|
||||||
// Librarian (document management)
|
// Librarian (document management)
|
||||||
librarianRequest: topic("librarian-request"),
|
librarianRequest: topic("librarian-request"),
|
||||||
librarianResponse: topic("librarian-response"),
|
librarianResponse: topic("librarian-response"),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"fastify": "^5.2.0",
|
"fastify": "^5.2.0",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"@mistralai/mistralai": "^1.0.0",
|
"@mistralai/mistralai": "^1.0.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
"openai": "^4.85.0",
|
"openai": "^4.85.0",
|
||||||
"pdfjs-dist": "^5.6.205"
|
"pdfjs-dist": "^5.6.205"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
1
ts/packages/flow/src/agent/mcp-tool/index.ts
Normal file
1
ts/packages/flow/src/agent/mcp-tool/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { McpToolService } from "./service.js";
|
||||||
160
ts/packages/flow/src/agent/mcp-tool/service.ts
Normal file
160
ts/packages/flow/src/agent/mcp-tool/service.ts
Normal file
|
|
@ -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<string, McpServiceConfig> = {};
|
||||||
|
|
||||||
|
constructor(config: ProcessorConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.registerSpecification(
|
||||||
|
new ConsumerSpec<ToolRequest>("request", this.onRequest.bind(this)),
|
||||||
|
);
|
||||||
|
this.registerSpecification(new ProducerSpec<ToolResponse>("response"));
|
||||||
|
|
||||||
|
this.registerConfigHandler(this.onMcpConfig.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onMcpConfig(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
version: number,
|
||||||
|
): Promise<void> {
|
||||||
|
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<string, string>;
|
||||||
|
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<string, string>,
|
||||||
|
flowCtx: FlowContext,
|
||||||
|
): Promise<void> {
|
||||||
|
const requestId = properties.id;
|
||||||
|
if (!requestId) return;
|
||||||
|
|
||||||
|
const responseProducer = flowCtx.flow.producer<ToolResponse>("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<string, unknown>,
|
||||||
|
): Promise<string | unknown> {
|
||||||
|
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<string, string> = {};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
* 4. Executes tools and feeds observations back to the LLM
|
* 4. Executes tools and feeds observations back to the LLM
|
||||||
* 5. Sends streaming AgentResponse chunks (thought, observation, answer, error)
|
* 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
|
* Python reference: trustgraph-flow/trustgraph/agent/react/service.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -29,19 +33,26 @@ import {
|
||||||
type DocumentRagResponse,
|
type DocumentRagResponse,
|
||||||
type TriplesQueryRequest,
|
type TriplesQueryRequest,
|
||||||
type TriplesQueryResponse,
|
type TriplesQueryResponse,
|
||||||
|
type ToolRequest,
|
||||||
|
type ToolResponse,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKnowledgeQueryTool,
|
createKnowledgeQueryTool,
|
||||||
createDocumentQueryTool,
|
createDocumentQueryTool,
|
||||||
createTriplesQueryTool,
|
createTriplesQueryTool,
|
||||||
|
createMcpTool,
|
||||||
} from "./tools.js";
|
} from "./tools.js";
|
||||||
import { buildReActPrompt } from "./prompt.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;
|
const MAX_ITERATIONS = 10;
|
||||||
|
|
||||||
export class AgentService extends FlowProcessor {
|
export class AgentService extends FlowProcessor {
|
||||||
|
/** Config-driven tools; null means "use hardcoded fallback". */
|
||||||
|
private configuredTools: AgentTool[] | null = null;
|
||||||
|
|
||||||
constructor(config: ProcessorConfig) {
|
constructor(config: ProcessorConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
|
|
@ -83,9 +94,175 @@ export class AgentService extends FlowProcessor {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// MCP tool invocation client
|
||||||
|
this.registerSpecification(
|
||||||
|
new RequestResponseSpec<ToolRequest, ToolResponse>(
|
||||||
|
"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");
|
console.log("[AgentService] Service initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Config-driven tool registration ----------
|
||||||
|
|
||||||
|
private async onToolsConfig(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
version: number,
|
||||||
|
): Promise<void> {
|
||||||
|
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<string, string>;
|
||||||
|
const tools: AgentTool[] = [];
|
||||||
|
|
||||||
|
for (const [_toolId, toolValue] of Object.entries(toolConfig)) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(toolValue) as Record<string, unknown>;
|
||||||
|
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<Record<string, string>>) ?? [];
|
||||||
|
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<GraphRagRequest, GraphRagResponse>("graph-rag"),
|
||||||
|
collection,
|
||||||
|
);
|
||||||
|
return { ...tool, execute: live.execute };
|
||||||
|
}
|
||||||
|
case "document-query": {
|
||||||
|
const live = createDocumentQueryTool(
|
||||||
|
flowCtx.flow.requestor<DocumentRagRequest, DocumentRagResponse>("doc-rag"),
|
||||||
|
collection,
|
||||||
|
);
|
||||||
|
return { ...tool, execute: live.execute };
|
||||||
|
}
|
||||||
|
case "triples-query": {
|
||||||
|
const live = createTriplesQueryTool(
|
||||||
|
flowCtx.flow.requestor<TriplesQueryRequest, TriplesQueryResponse>("triples"),
|
||||||
|
collection,
|
||||||
|
);
|
||||||
|
return { ...tool, execute: live.execute };
|
||||||
|
}
|
||||||
|
case "mcp-tool": {
|
||||||
|
const live = createMcpTool(
|
||||||
|
flowCtx.flow.requestor<ToolRequest, ToolResponse>("mcp-tool"),
|
||||||
|
tool.name,
|
||||||
|
tool.description,
|
||||||
|
tool.args,
|
||||||
|
);
|
||||||
|
return { ...tool, execute: live.execute };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async onRequest(
|
private async onRequest(
|
||||||
msg: AgentRequest,
|
msg: AgentRequest,
|
||||||
properties: Record<string, string>,
|
properties: Record<string, string>,
|
||||||
|
|
@ -97,21 +274,31 @@ export class AgentService extends FlowProcessor {
|
||||||
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build tools from flow requestors
|
// Build tools — config-driven or hardcoded fallback
|
||||||
const tools: AgentTool[] = [
|
let tools: AgentTool[];
|
||||||
createKnowledgeQueryTool(
|
|
||||||
flowCtx.flow.requestor<GraphRagRequest, GraphRagResponse>("graph-rag"),
|
if (this.configuredTools) {
|
||||||
msg.collection,
|
tools = this.wireTools(this.configuredTools, flowCtx, msg.collection);
|
||||||
),
|
} else {
|
||||||
createDocumentQueryTool(
|
// Hardcoded fallback (backward compat)
|
||||||
flowCtx.flow.requestor<DocumentRagRequest, DocumentRagResponse>("doc-rag"),
|
tools = [
|
||||||
msg.collection,
|
createKnowledgeQueryTool(
|
||||||
),
|
flowCtx.flow.requestor<GraphRagRequest, GraphRagResponse>("graph-rag"),
|
||||||
createTriplesQueryTool(
|
msg.collection,
|
||||||
flowCtx.flow.requestor<TriplesQueryRequest, TriplesQueryResponse>("triples"),
|
),
|
||||||
msg.collection,
|
createDocumentQueryTool(
|
||||||
),
|
flowCtx.flow.requestor<DocumentRagRequest, DocumentRagResponse>("doc-rag"),
|
||||||
];
|
msg.collection,
|
||||||
|
),
|
||||||
|
createTriplesQueryTool(
|
||||||
|
flowCtx.flow.requestor<TriplesQueryRequest, TriplesQueryResponse>("triples"),
|
||||||
|
msg.collection,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tool filtering by group and state
|
||||||
|
tools = filterToolsByGroupAndState(tools, msg.group, msg.state);
|
||||||
|
|
||||||
// Build the ReAct prompt
|
// Build the ReAct prompt
|
||||||
const { system, prompt: initialPrompt } = buildReActPrompt(
|
const { system, prompt: initialPrompt } = buildReActPrompt(
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ import type {
|
||||||
DocumentRagResponse,
|
DocumentRagResponse,
|
||||||
TriplesQueryRequest,
|
TriplesQueryRequest,
|
||||||
TriplesQueryResponse,
|
TriplesQueryResponse,
|
||||||
|
ToolRequest,
|
||||||
|
ToolResponse,
|
||||||
Term,
|
Term,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
|
||||||
import type { AgentTool } from "./types.js";
|
import type { AgentTool, ToolArg } from "./types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Term to a human-readable string.
|
* 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<ToolRequest, ToolResponse>,
|
||||||
|
toolName: string,
|
||||||
|
description: string,
|
||||||
|
args: ToolArg[],
|
||||||
|
): AgentTool {
|
||||||
|
return {
|
||||||
|
name: toolName,
|
||||||
|
description,
|
||||||
|
args,
|
||||||
|
async execute(input: string): Promise<string> {
|
||||||
|
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";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ export interface AgentTool {
|
||||||
description: string;
|
description: string;
|
||||||
args: ToolArg[];
|
args: ToolArg[];
|
||||||
execute: (input: string) => Promise<string>;
|
execute: (input: string) => Promise<string>;
|
||||||
|
/** Full tool config from config-push (used by tool filtering). */
|
||||||
|
config?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReActState =
|
export type ReActState =
|
||||||
|
|
|
||||||
61
ts/packages/flow/src/agent/tool-filter.ts
Normal file
61
ts/packages/flow/src/agent/tool-filter.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ const FLOW_SERVICES: ReadonlyMap<string, { request: string; response: string }>
|
||||||
["graph-embeddings", { request: "graph-embeddings-request", response: "graph-embeddings-response" }],
|
["graph-embeddings", { request: "graph-embeddings-request", response: "graph-embeddings-response" }],
|
||||||
["document-embeddings", { request: "doc-embeddings-request", response: "doc-embeddings-response" }],
|
["document-embeddings", { request: "doc-embeddings-request", response: "doc-embeddings-response" }],
|
||||||
["triples", { request: "triples-request", response: "triples-response" }],
|
["triples", { request: "triples-request", response: "triples-response" }],
|
||||||
|
["mcp-tool", { request: "mcp-tool-request", response: "mcp-tool-response" }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ export { ConfigService, type ConfigServiceConfig } from "./config/service.js";
|
||||||
// ReAct agent
|
// ReAct agent
|
||||||
export { AgentService } from "./agent/react/index.js";
|
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
|
// Librarian service
|
||||||
export { LibrarianService, type LibrarianServiceConfig } from "./librarian/service.js";
|
export { LibrarianService, type LibrarianServiceConfig } from "./librarian/service.js";
|
||||||
export { CollectionManager, type CollectionEntry } from "./librarian/collection-manager.js";
|
export { CollectionManager, type CollectionEntry } from "./librarian/collection-manager.js";
|
||||||
|
|
|
||||||
3
ts/pnpm-lock.yaml
generated
3
ts/pnpm-lock.yaml
generated
|
|
@ -104,6 +104,9 @@ importers:
|
||||||
'@mistralai/mistralai':
|
'@mistralai/mistralai':
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.15.1
|
version: 1.15.1
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.12.0
|
||||||
|
version: 1.29.0(zod@3.25.76)
|
||||||
'@qdrant/js-client-rest':
|
'@qdrant/js-client-rest':
|
||||||
specifier: ^1.13.0
|
specifier: ^1.13.0
|
||||||
version: 1.17.0(typescript@5.9.3)
|
version: 1.17.0(typescript@5.9.3)
|
||||||
|
|
|
||||||
28
ts/scripts/run-brave-mcp.sh
Executable file
28
ts/scripts/run-brave-mcp.sh
Executable file
|
|
@ -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
|
||||||
18
ts/scripts/run-mcp-tool.ts
Normal file
18
ts/scripts/run-mcp-tool.ts
Normal file
|
|
@ -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<void> {
|
||||||
|
await McpToolService.launch("mcp-tool");
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("MCP tool service failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -188,10 +188,68 @@ async function main(): Promise<void> {
|
||||||
// Librarian RPC (for PDF decoder)
|
// Librarian RPC (for PDF decoder)
|
||||||
"librarian-request": "tg.flow.librarian-request",
|
"librarian-request": "tg.flow.librarian-request",
|
||||||
"librarian-response": "tg.flow.librarian-response",
|
"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<string, string> = {
|
||||||
|
"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.");
|
console.log("\nConfiguration seeded successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue