2026-04-05 21:09:33 -05:00
|
|
|
/**
|
|
|
|
|
* TrustGraph MCP server.
|
|
|
|
|
*
|
|
|
|
|
* Exposes TrustGraph capabilities as MCP tools for AI assistants.
|
2026-04-05 22:44:45 -05:00
|
|
|
* Uses the vendored @trustgraph/client for all gateway communication.
|
2026-04-05 21:09:33 -05:00
|
|
|
*
|
|
|
|
|
* Python reference: trustgraph-mcp/trustgraph/mcp_server/mcp.py
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
|
|
|
import { z } from "zod";
|
2026-04-05 22:44:45 -05:00
|
|
|
import { createTrustGraphSocket, type BaseApi, type Term } from "@trustgraph/client";
|
2026-04-05 21:09:33 -05:00
|
|
|
|
|
|
|
|
export function createMcpServer(config: {
|
|
|
|
|
gatewayUrl: string;
|
2026-04-05 22:44:45 -05:00
|
|
|
user?: string;
|
2026-04-05 21:09:33 -05:00
|
|
|
token?: string;
|
|
|
|
|
flowId?: string;
|
|
|
|
|
}) {
|
|
|
|
|
const server = new McpServer({
|
|
|
|
|
name: "trustgraph",
|
|
|
|
|
version: "0.1.0",
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
const user = config.user ?? "mcp";
|
|
|
|
|
const socket: BaseApi = createTrustGraphSocket(
|
|
|
|
|
user,
|
|
|
|
|
config.token,
|
|
|
|
|
config.gatewayUrl,
|
|
|
|
|
);
|
2026-04-05 21:09:33 -05:00
|
|
|
|
|
|
|
|
const flowId = config.flowId ?? "default";
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
// ===================== Flow-scoped tools =====================
|
|
|
|
|
|
2026-04-05 21:09:33 -05:00
|
|
|
// --- Text Completion ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"text_completion",
|
|
|
|
|
"Run a text completion using the configured LLM",
|
|
|
|
|
{
|
|
|
|
|
system: z.string().describe("System prompt"),
|
|
|
|
|
prompt: z.string().describe("User prompt"),
|
|
|
|
|
},
|
|
|
|
|
async ({ system, prompt }) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
const response = await flow.textCompletion(system, prompt);
|
|
|
|
|
return { content: [{ type: "text" as const, text: response }] };
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Graph RAG ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"graph_rag",
|
|
|
|
|
"Query the knowledge graph using RAG",
|
|
|
|
|
{
|
|
|
|
|
query: z.string().describe("Natural language query"),
|
|
|
|
|
entity_limit: z.number().optional().describe("Max entities to retrieve"),
|
|
|
|
|
triple_limit: z.number().optional().describe("Max triples per entity"),
|
2026-04-05 22:44:45 -05:00
|
|
|
collection: z.string().optional().describe("Collection name"),
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
2026-04-05 22:44:45 -05:00
|
|
|
async ({ query, entity_limit, triple_limit, collection }) => {
|
|
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
const response = await flow.graphRag(
|
|
|
|
|
query,
|
|
|
|
|
{ entityLimit: entity_limit, tripleLimit: triple_limit },
|
|
|
|
|
collection,
|
|
|
|
|
);
|
|
|
|
|
return { content: [{ type: "text" as const, text: response }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Document RAG ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"document_rag",
|
|
|
|
|
"Query documents using RAG",
|
|
|
|
|
{
|
|
|
|
|
query: z.string().describe("Natural language query"),
|
|
|
|
|
doc_limit: z.number().optional().describe("Max documents to retrieve"),
|
|
|
|
|
collection: z.string().optional().describe("Collection name"),
|
|
|
|
|
},
|
|
|
|
|
async ({ query, doc_limit, collection }) => {
|
|
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
const response = await flow.documentRag(query, doc_limit, collection);
|
|
|
|
|
return { content: [{ type: "text" as const, text: response }] };
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Agent ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"agent",
|
|
|
|
|
"Ask the TrustGraph agent a question",
|
|
|
|
|
{
|
|
|
|
|
question: z.string().describe("Question for the agent"),
|
|
|
|
|
},
|
|
|
|
|
async ({ question }) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
let fullAnswer = "";
|
|
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
flow.agent(
|
|
|
|
|
question,
|
|
|
|
|
() => {}, // think — ignore for MCP
|
|
|
|
|
() => {}, // observe — ignore for MCP
|
|
|
|
|
(chunk, complete) => {
|
|
|
|
|
fullAnswer += chunk;
|
|
|
|
|
if (complete) resolve();
|
|
|
|
|
},
|
|
|
|
|
(err) => reject(new Error(err)),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { content: [{ type: "text" as const, text: fullAnswer }] };
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Embeddings ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"embeddings",
|
|
|
|
|
"Generate text embeddings",
|
|
|
|
|
{
|
|
|
|
|
text: z.array(z.string()).describe("Texts to embed"),
|
|
|
|
|
},
|
|
|
|
|
async ({ text }) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
const vectors = await flow.embeddings(text);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(vectors) }] };
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Triples Query ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"triples_query",
|
|
|
|
|
"Query the knowledge graph for triples matching a pattern",
|
|
|
|
|
{
|
|
|
|
|
s: z.string().optional().describe("Subject IRI"),
|
|
|
|
|
p: z.string().optional().describe("Predicate IRI"),
|
|
|
|
|
o: z.string().optional().describe("Object IRI or literal"),
|
|
|
|
|
limit: z.number().optional().describe("Max results"),
|
2026-04-05 22:44:45 -05:00
|
|
|
collection: z.string().optional().describe("Collection name"),
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
2026-04-05 22:44:45 -05:00
|
|
|
async ({ s, p, o, limit, collection }) => {
|
|
|
|
|
const flow = socket.flow(flowId);
|
|
|
|
|
const sTerm: Term | undefined = s ? { t: "i", i: s } : undefined;
|
|
|
|
|
const pTerm: Term | undefined = p ? { t: "i", i: p } : undefined;
|
|
|
|
|
const oTerm: Term | undefined = o ? { t: "i", i: o } : undefined;
|
|
|
|
|
const triples = await flow.triplesQuery(sTerm, pTerm, oTerm, limit, collection);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(triples, null, 2) }] };
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Graph Embeddings Query ---
|
|
|
|
|
server.tool(
|
|
|
|
|
"graph_embeddings_query",
|
|
|
|
|
"Find entities similar to a text query using vector embeddings",
|
|
|
|
|
{
|
|
|
|
|
query: z.string().describe("Text to find similar entities for"),
|
|
|
|
|
limit: z.number().optional().describe("Max results"),
|
2026-04-05 22:44:45 -05:00
|
|
|
collection: z.string().optional().describe("Collection name"),
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
2026-04-05 22:44:45 -05:00
|
|
|
async ({ query, limit, collection }) => {
|
|
|
|
|
const flow = socket.flow(flowId);
|
2026-04-05 21:09:33 -05:00
|
|
|
// First embed the query, then search
|
2026-04-05 22:44:45 -05:00
|
|
|
const vectors = await flow.embeddings([query]);
|
|
|
|
|
const entities = await flow.graphEmbeddingsQuery(
|
|
|
|
|
vectors[0],
|
|
|
|
|
limit ?? 10,
|
|
|
|
|
collection,
|
|
|
|
|
);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(entities, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ===================== Config tools =====================
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_config_all",
|
|
|
|
|
"Get all configuration values",
|
|
|
|
|
{},
|
|
|
|
|
async () => {
|
|
|
|
|
const cfg = socket.config();
|
|
|
|
|
const resp = await cfg.getConfigAll();
|
2026-04-05 21:09:33 -05:00
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_config",
|
2026-04-05 22:44:45 -05:00
|
|
|
"Get specific configuration values",
|
2026-04-05 21:09:33 -05:00
|
|
|
{
|
2026-04-05 22:44:45 -05:00
|
|
|
keys: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
type: z.string().describe("Config type"),
|
|
|
|
|
key: z.string().describe("Config key"),
|
|
|
|
|
}),
|
|
|
|
|
).describe("Config keys to retrieve"),
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
async ({ keys }) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const cfg = socket.config();
|
|
|
|
|
const resp = await cfg.getConfig(keys);
|
2026-04-05 21:09:33 -05:00
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"put_config",
|
|
|
|
|
"Set configuration values",
|
|
|
|
|
{
|
2026-04-05 22:44:45 -05:00
|
|
|
values: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
type: z.string().describe("Config type"),
|
|
|
|
|
key: z.string().describe("Config key"),
|
|
|
|
|
value: z.string().describe("Config value (JSON-encoded)"),
|
|
|
|
|
}),
|
|
|
|
|
).describe("Key-value entries to set"),
|
2026-04-05 21:09:33 -05:00
|
|
|
},
|
|
|
|
|
async ({ values }) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const cfg = socket.config();
|
|
|
|
|
const resp = await cfg.putConfig(values);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"delete_config",
|
|
|
|
|
"Delete a configuration entry",
|
|
|
|
|
{
|
|
|
|
|
type: z.string().describe("Config type"),
|
|
|
|
|
key: z.string().describe("Config key"),
|
|
|
|
|
},
|
|
|
|
|
async ({ type, key }) => {
|
|
|
|
|
const cfg = socket.config();
|
|
|
|
|
const resp = await cfg.deleteConfig({ type, key });
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ===================== Flow management tools =====================
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_flows",
|
|
|
|
|
"List all available flows",
|
|
|
|
|
{},
|
|
|
|
|
async () => {
|
|
|
|
|
const flows = socket.flows();
|
|
|
|
|
const ids = await flows.getFlows();
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(ids, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_flow",
|
|
|
|
|
"Get a specific flow definition",
|
|
|
|
|
{
|
|
|
|
|
flow_id: z.string().describe("Flow ID to retrieve"),
|
|
|
|
|
},
|
|
|
|
|
async ({ flow_id }) => {
|
|
|
|
|
const flows = socket.flows();
|
|
|
|
|
const def = await flows.getFlow(flow_id);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(def, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"start_flow",
|
|
|
|
|
"Start a flow instance",
|
|
|
|
|
{
|
|
|
|
|
flow_id: z.string().describe("Flow ID"),
|
|
|
|
|
blueprint_name: z.string().describe("Blueprint name"),
|
|
|
|
|
description: z.string().describe("Flow description"),
|
|
|
|
|
parameters: z.record(z.unknown()).optional().describe("Optional flow parameters"),
|
|
|
|
|
},
|
|
|
|
|
async ({ flow_id, blueprint_name, description, parameters }) => {
|
|
|
|
|
const flows = socket.flows();
|
|
|
|
|
const resp = await flows.startFlow(flow_id, blueprint_name, description, parameters);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"stop_flow",
|
|
|
|
|
"Stop a running flow",
|
|
|
|
|
{
|
|
|
|
|
flow_id: z.string().describe("Flow ID to stop"),
|
|
|
|
|
},
|
|
|
|
|
async ({ flow_id }) => {
|
|
|
|
|
const flows = socket.flows();
|
|
|
|
|
const resp = await flows.stopFlow(flow_id);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ===================== Library (document) tools =====================
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_documents",
|
|
|
|
|
"List all documents in the library",
|
|
|
|
|
{},
|
|
|
|
|
async () => {
|
|
|
|
|
const lib = socket.librarian();
|
|
|
|
|
const docs = await lib.getDocuments();
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(docs, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"load_document",
|
|
|
|
|
"Upload a document to the library",
|
|
|
|
|
{
|
|
|
|
|
document: z.string().describe("Base64-encoded document content"),
|
|
|
|
|
mime_type: z.string().describe("Document MIME type"),
|
|
|
|
|
title: z.string().describe("Document title"),
|
|
|
|
|
comments: z.string().optional().describe("Additional comments"),
|
|
|
|
|
tags: z.array(z.string()).optional().describe("Document tags"),
|
|
|
|
|
id: z.string().optional().describe("Optional document ID"),
|
|
|
|
|
},
|
|
|
|
|
async ({ document, mime_type, title, comments, tags, id }) => {
|
|
|
|
|
const lib = socket.librarian();
|
|
|
|
|
const resp = await lib.loadDocument(
|
|
|
|
|
document,
|
|
|
|
|
mime_type,
|
|
|
|
|
title,
|
|
|
|
|
comments ?? "",
|
|
|
|
|
tags ?? [],
|
|
|
|
|
id,
|
|
|
|
|
);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"remove_document",
|
|
|
|
|
"Remove a document from the library",
|
|
|
|
|
{
|
|
|
|
|
id: z.string().describe("Document ID to remove"),
|
|
|
|
|
collection: z.string().optional().describe("Collection name"),
|
|
|
|
|
},
|
|
|
|
|
async ({ id, collection }) => {
|
|
|
|
|
const lib = socket.librarian();
|
|
|
|
|
const resp = await lib.removeDocument(id, collection);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ===================== Prompt tools =====================
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_prompts",
|
|
|
|
|
"List available prompt templates",
|
|
|
|
|
{},
|
|
|
|
|
async () => {
|
|
|
|
|
const cfg = socket.config();
|
|
|
|
|
const prompts = await cfg.getPrompts();
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(prompts, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_prompt",
|
|
|
|
|
"Get a specific prompt template",
|
|
|
|
|
{
|
|
|
|
|
id: z.string().describe("Prompt template ID"),
|
|
|
|
|
},
|
|
|
|
|
async ({ id }) => {
|
|
|
|
|
const cfg = socket.config();
|
|
|
|
|
const prompt = await cfg.getPrompt(id);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(prompt, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ===================== Knowledge core tools =====================
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"get_knowledge_cores",
|
|
|
|
|
"List available knowledge graph cores",
|
|
|
|
|
{},
|
|
|
|
|
async () => {
|
|
|
|
|
const knowledge = socket.knowledge();
|
|
|
|
|
const cores = await knowledge.getKnowledgeCores();
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(cores, null, 2) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"delete_kg_core",
|
|
|
|
|
"Delete a knowledge graph core",
|
|
|
|
|
{
|
|
|
|
|
id: z.string().describe("Knowledge core ID"),
|
|
|
|
|
collection: z.string().optional().describe("Collection name"),
|
|
|
|
|
},
|
|
|
|
|
async ({ id, collection }) => {
|
|
|
|
|
const knowledge = socket.knowledge();
|
|
|
|
|
const resp = await knowledge.deleteKgCore(id, collection);
|
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.tool(
|
|
|
|
|
"load_kg_core",
|
|
|
|
|
"Load a knowledge graph core",
|
|
|
|
|
{
|
|
|
|
|
id: z.string().describe("Knowledge core ID"),
|
|
|
|
|
flow: z.string().describe("Flow to use for loading"),
|
|
|
|
|
collection: z.string().optional().describe("Collection name"),
|
|
|
|
|
},
|
|
|
|
|
async ({ id, flow, collection }) => {
|
|
|
|
|
const knowledge = socket.knowledge();
|
|
|
|
|
const resp = await knowledge.loadKgCore(id, flow, collection);
|
2026-04-05 21:09:33 -05:00
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { server, socket };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(): Promise<void> {
|
|
|
|
|
const { server, socket } = createMcpServer({
|
|
|
|
|
gatewayUrl: process.env.GATEWAY_URL ?? "ws://localhost:8088/api/v1/socket",
|
2026-04-05 22:44:45 -05:00
|
|
|
user: process.env.USER_ID ?? "mcp",
|
2026-04-05 21:09:33 -05:00
|
|
|
token: process.env.GATEWAY_SECRET,
|
|
|
|
|
flowId: process.env.FLOW_ID ?? "default",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const transport = new StdioServerTransport();
|
|
|
|
|
await server.connect(transport);
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
process.on("SIGINT", () => {
|
|
|
|
|
socket.close();
|
2026-04-05 21:09:33 -05:00
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
}
|