mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-06 11:52:10 +02:00
Migrate strict Effect runtime surfaces
This commit is contained in:
parent
f6878d4dd7
commit
b4ee2b691f
35 changed files with 1717 additions and 1410 deletions
|
|
@ -1,14 +1,21 @@
|
|||
import {OpenAiClient, OpenAiLanguageModel} from "@effect/ai-openai";
|
||||
import {BunHttpServer, BunRuntime} from "@effect/platform-bun";
|
||||
import {createTrustGraphSocket, type BaseApi, type Term as ClientTerm} from "@trustgraph/client";
|
||||
import {Context, Effect, Layer, Redacted} from "effect";
|
||||
import {Config, Context, Effect, Layer, Redacted} from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
import {LanguageModel, McpServer, Prompt, Tool, Toolkit} from "effect/unstable/ai";
|
||||
import {FetchHttpClient, HttpRouter} from "effect/unstable/http";
|
||||
import {HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi} from "effect/unstable/httpapi";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
const annotateTool = <T extends Tool.Any>(
|
||||
tool: T,
|
||||
const annotateTool = <Name extends string, Config extends {
|
||||
readonly parameters: S.Top
|
||||
readonly success: S.Top
|
||||
readonly failure: S.Top
|
||||
readonly failureMode: Tool.FailureMode
|
||||
}, Requirements>(
|
||||
tool: Tool.Tool<Name, Config, Requirements>,
|
||||
annotations: {
|
||||
readonly title: string
|
||||
readonly readOnly: boolean
|
||||
|
|
@ -17,14 +24,14 @@ const annotateTool = <T extends Tool.Any>(
|
|||
readonly openWorld: boolean
|
||||
readonly strict?: boolean
|
||||
},
|
||||
): T =>
|
||||
): Tool.Tool<Name, Config, Requirements> =>
|
||||
tool
|
||||
.annotate(Tool.Title, annotations.title)
|
||||
.annotate(Tool.Readonly, annotations.readOnly)
|
||||
.annotate(Tool.Destructive, annotations.destructive)
|
||||
.annotate(Tool.Idempotent, annotations.idempotent)
|
||||
.annotate(Tool.OpenWorld, annotations.openWorld)
|
||||
.annotate(Tool.Strict, annotations.strict ?? true) as T
|
||||
.annotate(Tool.Strict, annotations.strict ?? true)
|
||||
|
||||
class PromptSummary extends S.Class<PromptSummary>("PromptSummary")(
|
||||
{
|
||||
|
|
@ -1217,18 +1224,14 @@ export interface TrustGraphMcpConfigShape {
|
|||
readonly version: string
|
||||
readonly mcpPath: HttpRouter.PathInput
|
||||
readonly openAiModel: string
|
||||
readonly openAiApiKey: string | undefined
|
||||
readonly openAiApiKey: Redacted.Redacted | undefined
|
||||
readonly port: number
|
||||
}
|
||||
|
||||
const readNonEmpty = (value: string | undefined): string | undefined =>
|
||||
value !== undefined && value.length > 0 ? value : undefined
|
||||
|
||||
const resolvePort = (value: number | undefined): number => {
|
||||
if (value !== undefined) {
|
||||
return value
|
||||
}
|
||||
const raw = readNonEmpty(process.env.PORT)
|
||||
const parsePort = (raw: string | undefined): number => {
|
||||
if (raw === undefined) {
|
||||
return 3000
|
||||
}
|
||||
|
|
@ -1236,28 +1239,46 @@ const resolvePort = (value: number | undefined): number => {
|
|||
return Number.isFinite(parsed) ? parsed : 3000
|
||||
}
|
||||
|
||||
export const loadTrustGraphMcpConfig = Effect.fn("loadTrustGraphMcpConfig")(function*(
|
||||
options: TrustGraphMcpOptions = {},
|
||||
) {
|
||||
const gatewayUrl = O.getOrUndefined(yield* Config.string("GATEWAY_URL").pipe(Config.option))
|
||||
const user = O.getOrUndefined(yield* Config.string("USER_ID").pipe(Config.option))
|
||||
const gatewaySecret = O.getOrUndefined(yield* Config.string("GATEWAY_SECRET").pipe(Config.option))
|
||||
const token = readNonEmpty(gatewaySecret)
|
||||
const flowId = O.getOrUndefined(yield* Config.string("FLOW_ID").pipe(Config.option))
|
||||
const openAiModel = O.getOrUndefined(yield* Config.string("OPENAI_MODEL").pipe(Config.option))
|
||||
const openAiApiKey = O.getOrUndefined(yield* Config.redacted("OPENAI_API_KEY").pipe(Config.option))
|
||||
const openAiToken = O.getOrUndefined(yield* Config.redacted("OPENAI_TOKEN").pipe(Config.option))
|
||||
const port = O.getOrUndefined(yield* Config.string("PORT").pipe(Config.option))
|
||||
|
||||
return {
|
||||
gatewayUrl: options.gatewayUrl ?? gatewayUrl ?? "ws://localhost:8088/api/v1/rpc",
|
||||
user: options.user ?? user ?? "mcp",
|
||||
token: options.token ?? token,
|
||||
flowId: options.flowId ?? flowId ?? "default",
|
||||
name: options.name ?? "trustgraph",
|
||||
version: options.version ?? "0.1.0",
|
||||
mcpPath: options.mcpPath ?? "/mcp",
|
||||
openAiModel: options.openAiModel ?? openAiModel ?? "gpt-4.1",
|
||||
openAiApiKey: options.openAiApiKey === undefined
|
||||
? openAiApiKey ?? openAiToken
|
||||
: Redacted.make(options.openAiApiKey),
|
||||
port: options.port ?? parsePort(readNonEmpty(port)),
|
||||
}
|
||||
})
|
||||
|
||||
export const resolveTrustGraphMcpConfig = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
): TrustGraphMcpConfigShape => ({
|
||||
gatewayUrl: options.gatewayUrl ?? process.env.GATEWAY_URL ?? "ws://localhost:8088/api/v1/rpc",
|
||||
user: options.user ?? process.env.USER_ID ?? "mcp",
|
||||
token: options.token ?? readNonEmpty(process.env.GATEWAY_SECRET),
|
||||
flowId: options.flowId ?? process.env.FLOW_ID ?? "default",
|
||||
name: options.name ?? "trustgraph",
|
||||
version: options.version ?? "0.1.0",
|
||||
mcpPath: options.mcpPath ?? "/mcp",
|
||||
openAiModel: options.openAiModel ?? process.env.OPENAI_MODEL ?? "gpt-4.1",
|
||||
openAiApiKey: options.openAiApiKey ?? readNonEmpty(process.env.OPENAI_API_KEY) ?? readNonEmpty(process.env.OPENAI_TOKEN),
|
||||
port: resolvePort(options.port),
|
||||
})
|
||||
): TrustGraphMcpConfigShape => Effect.runSync(loadTrustGraphMcpConfig(options))
|
||||
|
||||
export class TrustGraphMcpConfig extends Context.Service<TrustGraphMcpConfig, TrustGraphMcpConfigShape>()(
|
||||
"@trustgraph/mcp/server-effect/TrustGraphMcpConfig",
|
||||
) {
|
||||
static readonly layer = (options: TrustGraphMcpOptions = {}) =>
|
||||
Layer.succeed(
|
||||
Layer.effect(
|
||||
TrustGraphMcpConfig,
|
||||
TrustGraphMcpConfig.of(resolveTrustGraphMcpConfig(options)),
|
||||
loadTrustGraphMcpConfig(options).pipe(Effect.map(TrustGraphMcpConfig.of)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1278,17 +1299,14 @@ export class TrustGraphSocket extends Context.Service<TrustGraphSocket, BaseApi>
|
|||
}
|
||||
|
||||
const toErrorMessage = (cause: unknown): string => {
|
||||
if (cause instanceof Error && cause.message.length > 0) {
|
||||
if (Predicate.isError(cause) && cause.message.length > 0) {
|
||||
return cause.message
|
||||
}
|
||||
if (typeof cause === "string" && cause.length > 0) {
|
||||
return cause
|
||||
}
|
||||
if (cause !== null && typeof cause === "object" && "message" in cause) {
|
||||
const message = (cause as { readonly message?: unknown }).message
|
||||
if (typeof message === "string" && message.length > 0) {
|
||||
return message
|
||||
}
|
||||
if (Predicate.isObject(cause) && Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) && cause.message.length > 0) {
|
||||
return cause.message
|
||||
}
|
||||
return "TrustGraph MCP tool failed"
|
||||
}
|
||||
|
|
@ -1313,16 +1331,15 @@ const decodeJsonArrayOrFail = <E>(
|
|||
const asIriTerm = (value: string | undefined): ClientTerm | undefined =>
|
||||
value !== undefined && value.length > 0 ? {t: "i", i: value} : undefined
|
||||
|
||||
const openAiApiKeyOptions = (apiKey: string | undefined) =>
|
||||
const openAiApiKeyOptions = (apiKey: Redacted.Redacted | undefined) =>
|
||||
apiKey === undefined
|
||||
? {}
|
||||
: {apiKey: Redacted.make(apiKey)}
|
||||
: {apiKey}
|
||||
|
||||
export const makeOpenAiProviderLayer = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
) => {
|
||||
const config = resolveTrustGraphMcpConfig(options)
|
||||
return OpenAiLanguageModel.layer({
|
||||
const makeOpenAiProviderLayerFromConfig = (
|
||||
config: TrustGraphMcpConfigShape,
|
||||
) =>
|
||||
OpenAiLanguageModel.layer({
|
||||
model: config.openAiModel,
|
||||
config: {
|
||||
strictJsonSchema: true,
|
||||
|
|
@ -1331,7 +1348,15 @@ export const makeOpenAiProviderLayer = (
|
|||
Layer.provide(OpenAiClient.layer(openAiApiKeyOptions(config.openAiApiKey))),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
}
|
||||
|
||||
export const makeOpenAiProviderLayer = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
) =>
|
||||
Layer.unwrap(
|
||||
loadTrustGraphMcpConfig(options).pipe(
|
||||
Effect.map(makeOpenAiProviderLayerFromConfig),
|
||||
),
|
||||
)
|
||||
|
||||
export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
||||
Effect.gen(function*() {
|
||||
|
|
@ -1344,33 +1369,32 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
const response = yield* model.generateText({
|
||||
prompt: Prompt.make(prompt).pipe(Prompt.setSystem(system)),
|
||||
})
|
||||
return new TextCompletionSuccess({text: response.text})
|
||||
return TextCompletionSuccess.make({text: response.text})
|
||||
}),
|
||||
|
||||
graph_rag: ({query, entity_limit, triple_limit, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const response = await socket.flow(config.flowId).graphRag(
|
||||
try: () =>
|
||||
socket.flow(config.flowId).graphRag(
|
||||
query,
|
||||
{
|
||||
...(entity_limit !== undefined ? {entityLimit: entity_limit} : {}),
|
||||
...(triple_limit !== undefined ? {tripleLimit: triple_limit} : {}),
|
||||
},
|
||||
collection,
|
||||
)
|
||||
return new GraphRagSuccess({text: response})
|
||||
},
|
||||
catch: (cause) => new GraphRagError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
),
|
||||
catch: (cause) => GraphRagError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((text) => GraphRagSuccess.make({text})),
|
||||
),
|
||||
|
||||
document_rag: ({query, doc_limit, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const response = await socket.flow(config.flowId).documentRag(query, doc_limit, collection)
|
||||
return new DocumentRagSuccess({text: response})
|
||||
},
|
||||
catch: (cause) => new DocumentRagError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.flow(config.flowId).documentRag(query, doc_limit, collection),
|
||||
catch: (cause) => DocumentRagError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((text) => DocumentRagSuccess.make({text})),
|
||||
),
|
||||
|
||||
agent: ({question}) =>
|
||||
Effect.callback<AgentSuccess, AgentError>((resume) => {
|
||||
|
|
@ -1382,62 +1406,65 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
(chunk, complete) => {
|
||||
fullAnswer += chunk
|
||||
if (complete) {
|
||||
resume(Effect.succeed(new AgentSuccess({text: fullAnswer})))
|
||||
resume(Effect.succeed(AgentSuccess.make({text: fullAnswer})))
|
||||
}
|
||||
},
|
||||
(cause) => resume(Effect.fail(new AgentError({cause, message: toErrorMessage(cause)}))),
|
||||
(cause) => resume(Effect.fail(AgentError.make({cause, message: toErrorMessage(cause)}))),
|
||||
)
|
||||
}),
|
||||
|
||||
embeddings: ({text}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const vectors = await socket.flow(config.flowId).embeddings([...text])
|
||||
return new EmbeddingsSuccess({vectors})
|
||||
},
|
||||
catch: (cause) => new EmbeddingsError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.flow(config.flowId).embeddings([...text]),
|
||||
catch: (cause) => EmbeddingsError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((vectors) => EmbeddingsSuccess.make({vectors})),
|
||||
),
|
||||
|
||||
triples_query: ({s, p, o, limit, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const triples = await socket.flow(config.flowId).triplesQuery(
|
||||
try: () =>
|
||||
socket.flow(config.flowId).triplesQuery(
|
||||
asIriTerm(s),
|
||||
asIriTerm(p),
|
||||
asIriTerm(o),
|
||||
limit,
|
||||
collection,
|
||||
)
|
||||
return new TriplesQuerySuccess({triples})
|
||||
},
|
||||
catch: (cause) => new TriplesQueryError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
),
|
||||
catch: (cause) => TriplesQueryError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((triples) => TriplesQuerySuccess.make({triples})),
|
||||
),
|
||||
|
||||
graph_embeddings_query: ({query, limit, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const vectors = await socket.flow(config.flowId).embeddings([query])
|
||||
const entities = await socket.flow(config.flowId).graphEmbeddingsQuery(
|
||||
vectors[0] ?? [],
|
||||
limit ?? 10,
|
||||
collection,
|
||||
)
|
||||
return new GraphEmbeddingsQuerySuccess({entities})
|
||||
},
|
||||
catch: (cause) => new GraphEmbeddingsQueryError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.flow(config.flowId).embeddings([query]),
|
||||
catch: (cause) => GraphEmbeddingsQueryError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((vectors) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.flow(config.flowId).graphEmbeddingsQuery(
|
||||
vectors[0] ?? [],
|
||||
limit ?? 10,
|
||||
collection,
|
||||
),
|
||||
catch: (cause) => GraphEmbeddingsQueryError.make({cause, message: toErrorMessage(cause)}),
|
||||
})
|
||||
),
|
||||
Effect.map((entities) => GraphEmbeddingsQuerySuccess.make({entities})),
|
||||
),
|
||||
|
||||
get_config_all: () =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.config().getConfigAll(),
|
||||
catch: (cause) => new GetConfigAllError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => GetConfigAllError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new GetConfigAllError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => GetConfigAllError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((config) => new GetConfigAllSuccess({config})),
|
||||
Effect.map((config) => GetConfigAllSuccess.make({config})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1445,14 +1472,14 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
get_config: ({keys}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.config().getConfig(keys.map(({type, key}) => ({type, key}))),
|
||||
catch: (cause) => new GetConfigError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => GetConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new GetConfigError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => GetConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((config) => new GetConfigSuccess({config})),
|
||||
Effect.map((config) => GetConfigSuccess.make({config})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1460,14 +1487,14 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
put_config: ({values}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.config().putConfig(values.map(({type, key, value}) => ({type, key, value}))),
|
||||
catch: (cause) => new PutConfigError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => PutConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new PutConfigError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => PutConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new PutConfigSuccess({response})),
|
||||
Effect.map((response) => PutConfigSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1475,56 +1502,58 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
delete_config: ({type, key}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.config().deleteConfig({type, key}),
|
||||
catch: (cause) => new DeleteConfigError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => DeleteConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new DeleteConfigError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => DeleteConfigError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new DeleteConfigSuccess({response})),
|
||||
Effect.map((response) => DeleteConfigSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
get_flows: () =>
|
||||
Effect.tryPromise({
|
||||
try: async () => new GetFlowsSuccess({flow_ids: await socket.flows().getFlows()}),
|
||||
catch: (cause) => new GetFlowsError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.flows().getFlows(),
|
||||
catch: (cause) => GetFlowsError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((flow_ids) => GetFlowsSuccess.make({flow_ids})),
|
||||
),
|
||||
|
||||
get_flow: ({flow_id}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.flows().getFlow(flow_id),
|
||||
catch: (cause) => new GetFlowError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => GetFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new GetFlowError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => GetFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((flow) => new GetFlowSuccess({flow})),
|
||||
Effect.map((flow) => GetFlowSuccess.make({flow})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
start_flow: ({flow_id, blueprint_name, description, parameters}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () =>
|
||||
try: () =>
|
||||
socket.flows().startFlow(
|
||||
flow_id,
|
||||
blueprint_name,
|
||||
description,
|
||||
parameters === undefined ? undefined : {...parameters},
|
||||
),
|
||||
catch: (cause) => new StartFlowError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => StartFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new StartFlowError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => StartFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new StartFlowSuccess({response})),
|
||||
Effect.map((response) => StartFlowSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1532,14 +1561,14 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
stop_flow: ({flow_id}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.flows().stopFlow(flow_id),
|
||||
catch: (cause) => new StopFlowError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => StopFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new StopFlowError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => StopFlowError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new StopFlowSuccess({response})),
|
||||
Effect.map((response) => StopFlowSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1547,21 +1576,21 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
get_documents: () =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.librarian().getDocuments(),
|
||||
catch: (cause) => new GetDocumentsError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => GetDocumentsError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonArrayOrFail(
|
||||
value,
|
||||
(cause) => new GetDocumentsError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => GetDocumentsError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((documents) => new GetDocumentsSuccess({documents})),
|
||||
Effect.map((documents) => GetDocumentsSuccess.make({documents})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
load_document: ({document, mime_type, title, comments, tags, id}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () =>
|
||||
try: () =>
|
||||
socket.librarian().loadDocument(
|
||||
document,
|
||||
mime_type,
|
||||
|
|
@ -1570,14 +1599,14 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
tags === undefined ? [] : [...tags],
|
||||
id,
|
||||
),
|
||||
catch: (cause) => new LoadDocumentError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => LoadDocumentError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new LoadDocumentError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => LoadDocumentError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new LoadDocumentSuccess({response})),
|
||||
Effect.map((response) => LoadDocumentSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1585,56 +1614,60 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
remove_document: ({id, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.librarian().removeDocument(id, collection),
|
||||
catch: (cause) => new RemoveDocumentError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => RemoveDocumentError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new RemoveDocumentError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => RemoveDocumentError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new RemoveDocumentSuccess({response})),
|
||||
Effect.map((response) => RemoveDocumentSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
get_prompts: () =>
|
||||
Effect.tryPromise({
|
||||
try: async () => new GetPromptsSuccess({prompts: await socket.config().getPrompts()}),
|
||||
catch: (cause) => new GetPromptsError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.config().getPrompts(),
|
||||
catch: (cause) => GetPromptsError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((prompts) => GetPromptsSuccess.make({prompts})),
|
||||
),
|
||||
|
||||
get_prompt: ({id}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.config().getPrompt(id),
|
||||
catch: (cause) => new GetPromptError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => GetPromptError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new GetPromptError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => GetPromptError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((prompt) => new GetPromptSuccess({prompt})),
|
||||
Effect.map((prompt) => GetPromptSuccess.make({prompt})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
get_knowledge_cores: () =>
|
||||
Effect.tryPromise({
|
||||
try: async () => new GetKnowledgeCoresSuccess({ids: await socket.knowledge().getKnowledgeCores()}),
|
||||
catch: (cause) => new GetKnowledgeCoresError({cause, message: toErrorMessage(cause)}),
|
||||
}),
|
||||
try: () => socket.knowledge().getKnowledgeCores(),
|
||||
catch: (cause) => GetKnowledgeCoresError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.map((ids) => GetKnowledgeCoresSuccess.make({ids})),
|
||||
),
|
||||
|
||||
delete_kg_core: ({id, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.knowledge().deleteKgCore(id, collection),
|
||||
catch: (cause) => new DeleteKgCoreError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => DeleteKgCoreError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new DeleteKgCoreError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => DeleteKgCoreError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new DeleteKgCoreSuccess({response})),
|
||||
Effect.map((response) => DeleteKgCoreSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1642,14 +1675,14 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
load_kg_core: ({id, flow, collection}) =>
|
||||
Effect.tryPromise({
|
||||
try: () => socket.knowledge().loadKgCore(id, flow, collection),
|
||||
catch: (cause) => new LoadKgCoreError({cause, message: toErrorMessage(cause)}),
|
||||
catch: (cause) => LoadKgCoreError.make({cause, message: toErrorMessage(cause)}),
|
||||
}).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeJsonOrFail(
|
||||
value,
|
||||
(cause) => new LoadKgCoreError({cause, message: toErrorMessage(cause)}),
|
||||
(cause) => LoadKgCoreError.make({cause, message: toErrorMessage(cause)}),
|
||||
).pipe(
|
||||
Effect.map((response) => new LoadKgCoreSuccess({response})),
|
||||
Effect.map((response) => LoadKgCoreSuccess.make({response})),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -1689,13 +1722,12 @@ export const TrustGraphMcpHttpApiRoutes = HttpApiBuilder.layer(
|
|||
Layer.provide(TrustGraphMcpHttpApiHandlers),
|
||||
)
|
||||
|
||||
export const makeTrustGraphMcpHttpLayer = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
const makeTrustGraphMcpHttpLayerFromConfig = (
|
||||
config: TrustGraphMcpConfigShape,
|
||||
) => {
|
||||
const config = resolveTrustGraphMcpConfig(options)
|
||||
const tools = McpServer.toolkit(TrustGraphMcpToolkit).pipe(
|
||||
Layer.provide(TrustGraphMcpToolkitLive),
|
||||
Layer.provide(makeOpenAiProviderLayer(config)),
|
||||
Layer.provide(makeOpenAiProviderLayerFromConfig(config)),
|
||||
)
|
||||
|
||||
return Layer.mergeAll(
|
||||
|
|
@ -1708,18 +1740,31 @@ export const makeTrustGraphMcpHttpLayer = (
|
|||
path: config.mcpPath,
|
||||
})),
|
||||
Layer.provide(TrustGraphSocket.layer),
|
||||
Layer.provide(TrustGraphMcpConfig.layer(config)),
|
||||
Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))),
|
||||
)
|
||||
}
|
||||
|
||||
export const makeTrustGraphMcpHttpServerLayer = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
) => {
|
||||
const config = resolveTrustGraphMcpConfig(options)
|
||||
return HttpRouter.serve(makeTrustGraphMcpHttpLayer(config)).pipe(
|
||||
Layer.provide(BunHttpServer.layer({port: config.port})),
|
||||
) =>
|
||||
Layer.unwrap(
|
||||
loadTrustGraphMcpConfig(options).pipe(
|
||||
Effect.map((config) =>
|
||||
HttpRouter.serve(makeTrustGraphMcpHttpLayerFromConfig(config)).pipe(
|
||||
Layer.provide(BunHttpServer.layer({port: config.port})),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
export const makeTrustGraphMcpHttpLayer = (
|
||||
options: TrustGraphMcpOptions = {},
|
||||
) =>
|
||||
Layer.unwrap(
|
||||
loadTrustGraphMcpConfig(options).pipe(
|
||||
Effect.map(makeTrustGraphMcpHttpLayerFromConfig),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const runHttp = (options: TrustGraphMcpOptions = {}): void => {
|
||||
Layer.launch(makeTrustGraphMcpHttpServerLayer(options)).pipe(BunRuntime.runMain)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,77 @@
|
|||
/**
|
||||
* TrustGraph MCP server.
|
||||
* TrustGraph MCP stdio compatibility server.
|
||||
*
|
||||
* Exposes TrustGraph capabilities as MCP tools for AI assistants.
|
||||
* Uses the vendored @trustgraph/client for all gateway communication.
|
||||
*
|
||||
* Python reference: trustgraph-mcp/trustgraph/mcp_server/mcp.py
|
||||
* This keeps the original @modelcontextprotocol/sdk entry points available,
|
||||
* while moving gateway calls, callback bridging, JSON encoding, and config
|
||||
* reads behind Effect values.
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { createTrustGraphSocket, type BaseApi, type Term } from "@trustgraph/client";
|
||||
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {NodeRuntime} from "@effect/platform-node";
|
||||
import {createTrustGraphSocket, type BaseApi, type Term} from "@trustgraph/client";
|
||||
import {Effect, Layer, ManagedRuntime} from "effect";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
import * as S from "effect/Schema";
|
||||
import {z} from "zod";
|
||||
import {loadTrustGraphMcpConfig} from "./server-effect.js";
|
||||
|
||||
interface ToolTextContent {
|
||||
readonly type: "text"
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
interface ToolTextResult extends Record<string, unknown> {
|
||||
readonly content: Array<ToolTextContent>
|
||||
}
|
||||
|
||||
class StdioMcpError extends S.TaggedErrorClass<StdioMcpError>()(
|
||||
"StdioMcpError",
|
||||
{
|
||||
cause: S.DefectWithStack,
|
||||
message: S.String,
|
||||
},
|
||||
) {
|
||||
}
|
||||
|
||||
const encodeJsonText = S.encodeUnknownEffect(S.UnknownFromJsonString);
|
||||
|
||||
const toErrorMessage = (cause: unknown): string => {
|
||||
if (Predicate.isError(cause) && cause.message.length > 0) {
|
||||
return cause.message;
|
||||
}
|
||||
if (Predicate.isString(cause) && cause.length > 0) {
|
||||
return cause;
|
||||
}
|
||||
if (Predicate.isObject(cause) && Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) && cause.message.length > 0) {
|
||||
return cause.message;
|
||||
}
|
||||
return "TrustGraph MCP stdio operation failed";
|
||||
};
|
||||
|
||||
const stdioMcpError = (cause: unknown) =>
|
||||
StdioMcpError.make({cause, message: toErrorMessage(cause)});
|
||||
|
||||
const textResult = (text: string): ToolTextResult => ({
|
||||
content: [{type: "text", text}],
|
||||
});
|
||||
|
||||
const gatewayRequest = <A>(request: () => Promise<A>) =>
|
||||
Effect.tryPromise({
|
||||
try: request,
|
||||
catch: stdioMcpError,
|
||||
});
|
||||
|
||||
const jsonText = (value: unknown) =>
|
||||
encodeJsonText(value).pipe(
|
||||
Effect.mapError(stdioMcpError),
|
||||
);
|
||||
|
||||
const runTextTool = (effect: Effect.Effect<string, StdioMcpError>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.map(textResult)));
|
||||
|
||||
const runJsonTool = (effect: Effect.Effect<unknown, StdioMcpError>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.flatMap(jsonText), Effect.map(textResult)));
|
||||
|
||||
export function createMcpServer(config: {
|
||||
gatewayUrl: string;
|
||||
|
|
@ -34,7 +95,6 @@ export function createMcpServer(config: {
|
|||
|
||||
// ===================== Flow-scoped tools =====================
|
||||
|
||||
// --- Text Completion ---
|
||||
server.tool(
|
||||
"text_completion",
|
||||
"Run a text completion using the configured LLM",
|
||||
|
|
@ -42,14 +102,10 @@ export function createMcpServer(config: {
|
|||
system: z.string().describe("System prompt"),
|
||||
prompt: z.string().describe("User prompt"),
|
||||
},
|
||||
async ({ system, prompt }) => {
|
||||
const flow = socket.flow(flowId);
|
||||
const response = await flow.textCompletion(system, prompt);
|
||||
return { content: [{ type: "text" as const, text: response }] };
|
||||
},
|
||||
({system, prompt}) =>
|
||||
runTextTool(gatewayRequest(() => socket.flow(flowId).textCompletion(system, prompt))),
|
||||
);
|
||||
|
||||
// --- Graph RAG ---
|
||||
server.tool(
|
||||
"graph_rag",
|
||||
"Query the knowledge graph using RAG",
|
||||
|
|
@ -59,21 +115,21 @@ export function createMcpServer(config: {
|
|||
triple_limit: z.number().optional().describe("Max triples per entity"),
|
||||
collection: z.string().optional().describe("Collection name"),
|
||||
},
|
||||
async ({ query, entity_limit, triple_limit, collection }) => {
|
||||
const flow = socket.flow(flowId);
|
||||
const response = await flow.graphRag(
|
||||
query,
|
||||
{
|
||||
...(entity_limit !== undefined ? { entityLimit: entity_limit } : {}),
|
||||
...(triple_limit !== undefined ? { tripleLimit: triple_limit } : {}),
|
||||
},
|
||||
collection,
|
||||
);
|
||||
return { content: [{ type: "text" as const, text: response }] };
|
||||
},
|
||||
({query, entity_limit, triple_limit, collection}) =>
|
||||
runTextTool(
|
||||
gatewayRequest(() =>
|
||||
socket.flow(flowId).graphRag(
|
||||
query,
|
||||
{
|
||||
...(entity_limit !== undefined ? {entityLimit: entity_limit} : {}),
|
||||
...(triple_limit !== undefined ? {tripleLimit: triple_limit} : {}),
|
||||
},
|
||||
collection,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// --- Document RAG ---
|
||||
server.tool(
|
||||
"document_rag",
|
||||
"Query documents using RAG",
|
||||
|
|
@ -82,56 +138,45 @@ export function createMcpServer(config: {
|
|||
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 }] };
|
||||
},
|
||||
({query, doc_limit, collection}) =>
|
||||
runTextTool(gatewayRequest(() => socket.flow(flowId).documentRag(query, doc_limit, collection))),
|
||||
);
|
||||
|
||||
// --- Agent ---
|
||||
server.tool(
|
||||
"agent",
|
||||
"Ask the TrustGraph agent a question",
|
||||
{
|
||||
question: z.string().describe("Question for the agent"),
|
||||
},
|
||||
async ({ question }) => {
|
||||
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 }] };
|
||||
},
|
||||
({question}) =>
|
||||
runTextTool(
|
||||
Effect.callback<string, StdioMcpError>((resume) => {
|
||||
let fullAnswer = "";
|
||||
socket.flow(flowId).agent(
|
||||
question,
|
||||
() => {},
|
||||
() => {},
|
||||
(chunk, complete) => {
|
||||
fullAnswer += chunk;
|
||||
if (complete) {
|
||||
resume(Effect.succeed(fullAnswer));
|
||||
}
|
||||
},
|
||||
(cause) => resume(Effect.fail(stdioMcpError(cause))),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// --- Embeddings ---
|
||||
server.tool(
|
||||
"embeddings",
|
||||
"Generate text embeddings",
|
||||
{
|
||||
text: z.array(z.string()).describe("Texts to embed"),
|
||||
},
|
||||
async ({ text }) => {
|
||||
const flow = socket.flow(flowId);
|
||||
const vectors = await flow.embeddings(text);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(vectors) }] };
|
||||
},
|
||||
({text}) => runJsonTool(gatewayRequest(() => socket.flow(flowId).embeddings(text))),
|
||||
);
|
||||
|
||||
// --- Triples Query ---
|
||||
server.tool(
|
||||
"triples_query",
|
||||
"Query the knowledge graph for triples matching a pattern",
|
||||
|
|
@ -142,17 +187,16 @@ export function createMcpServer(config: {
|
|||
limit: z.number().optional().describe("Max results"),
|
||||
collection: z.string().optional().describe("Collection name"),
|
||||
},
|
||||
async ({ s, p, o, limit, collection }) => {
|
||||
const flow = socket.flow(flowId);
|
||||
const sTerm: Term | undefined = s !== undefined && s.length > 0 ? { t: "i", i: s } : undefined;
|
||||
const pTerm: Term | undefined = p !== undefined && p.length > 0 ? { t: "i", i: p } : undefined;
|
||||
const oTerm: Term | undefined = o !== undefined && o.length > 0 ? { 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) }] };
|
||||
({s, p, o, limit, collection}) => {
|
||||
const sTerm: Term | undefined = s !== undefined && s.length > 0 ? {t: "i", i: s} : undefined;
|
||||
const pTerm: Term | undefined = p !== undefined && p.length > 0 ? {t: "i", i: p} : undefined;
|
||||
const oTerm: Term | undefined = o !== undefined && o.length > 0 ? {t: "i", i: o} : undefined;
|
||||
return runJsonTool(
|
||||
gatewayRequest(() => socket.flow(flowId).triplesQuery(sTerm, pTerm, oTerm, limit, collection)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// --- Graph Embeddings Query ---
|
||||
server.tool(
|
||||
"graph_embeddings_query",
|
||||
"Find entities similar to a text query using vector embeddings",
|
||||
|
|
@ -161,17 +205,20 @@ export function createMcpServer(config: {
|
|||
limit: z.number().optional().describe("Max results"),
|
||||
collection: z.string().optional().describe("Collection name"),
|
||||
},
|
||||
async ({ query, limit, collection }) => {
|
||||
const flow = socket.flow(flowId);
|
||||
// First embed the query, then search
|
||||
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) }] };
|
||||
},
|
||||
({query, limit, collection}) =>
|
||||
runJsonTool(
|
||||
gatewayRequest(() => socket.flow(flowId).embeddings([query])).pipe(
|
||||
Effect.flatMap((vectors) =>
|
||||
gatewayRequest(() =>
|
||||
socket.flow(flowId).graphEmbeddingsQuery(
|
||||
vectors[0] ?? [],
|
||||
limit ?? 10,
|
||||
collection,
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// ===================== Config tools =====================
|
||||
|
|
@ -180,11 +227,7 @@ export function createMcpServer(config: {
|
|||
"get_config_all",
|
||||
"Get all configuration values",
|
||||
{},
|
||||
async () => {
|
||||
const cfg = socket.config();
|
||||
const resp = await cfg.getConfigAll();
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
||||
},
|
||||
() => runJsonTool(gatewayRequest(() => socket.config().getConfigAll())),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -198,11 +241,7 @@ export function createMcpServer(config: {
|
|||
}),
|
||||
).describe("Config keys to retrieve"),
|
||||
},
|
||||
async ({ keys }) => {
|
||||
const cfg = socket.config();
|
||||
const resp = await cfg.getConfig(keys);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(resp, null, 2) }] };
|
||||
},
|
||||
({keys}) => runJsonTool(gatewayRequest(() => socket.config().getConfig(keys))),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -217,11 +256,7 @@ export function createMcpServer(config: {
|
|||
}),
|
||||
).describe("Key-value entries to set"),
|
||||
},
|
||||
async ({ values }) => {
|
||||
const cfg = socket.config();
|
||||
const resp = await cfg.putConfig(values);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
||||
},
|
||||
({values}) => runJsonTool(gatewayRequest(() => socket.config().putConfig(values))),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -231,11 +266,7 @@ export function createMcpServer(config: {
|
|||
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) }] };
|
||||
},
|
||||
({type, key}) => runJsonTool(gatewayRequest(() => socket.config().deleteConfig({type, key}))),
|
||||
);
|
||||
|
||||
// ===================== Flow management tools =====================
|
||||
|
|
@ -244,11 +275,7 @@ export function createMcpServer(config: {
|
|||
"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) }] };
|
||||
},
|
||||
() => runJsonTool(gatewayRequest(() => socket.flows().getFlows())),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -257,11 +284,7 @@ export function createMcpServer(config: {
|
|||
{
|
||||
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) }] };
|
||||
},
|
||||
({flow_id}) => runJsonTool(gatewayRequest(() => socket.flows().getFlow(flow_id))),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -273,11 +296,10 @@ export function createMcpServer(config: {
|
|||
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) }] };
|
||||
},
|
||||
({flow_id, blueprint_name, description, parameters}) =>
|
||||
runJsonTool(
|
||||
gatewayRequest(() => socket.flows().startFlow(flow_id, blueprint_name, description, parameters)),
|
||||
),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -286,11 +308,7 @@ export function createMcpServer(config: {
|
|||
{
|
||||
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) }] };
|
||||
},
|
||||
({flow_id}) => runJsonTool(gatewayRequest(() => socket.flows().stopFlow(flow_id))),
|
||||
);
|
||||
|
||||
// ===================== Library (document) tools =====================
|
||||
|
|
@ -299,11 +317,7 @@ export function createMcpServer(config: {
|
|||
"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) }] };
|
||||
},
|
||||
() => runJsonTool(gatewayRequest(() => socket.librarian().getDocuments())),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -317,18 +331,19 @@ export function createMcpServer(config: {
|
|||
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) }] };
|
||||
},
|
||||
({document, mime_type, title, comments, tags, id}) =>
|
||||
runJsonTool(
|
||||
gatewayRequest(() =>
|
||||
socket.librarian().loadDocument(
|
||||
document,
|
||||
mime_type,
|
||||
title,
|
||||
comments ?? "",
|
||||
tags ?? [],
|
||||
id,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -338,11 +353,7 @@ export function createMcpServer(config: {
|
|||
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) }] };
|
||||
},
|
||||
({id, collection}) => runJsonTool(gatewayRequest(() => socket.librarian().removeDocument(id, collection))),
|
||||
);
|
||||
|
||||
// ===================== Prompt tools =====================
|
||||
|
|
@ -351,11 +362,7 @@ export function createMcpServer(config: {
|
|||
"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) }] };
|
||||
},
|
||||
() => runJsonTool(gatewayRequest(() => socket.config().getPrompts())),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -364,11 +371,7 @@ export function createMcpServer(config: {
|
|||
{
|
||||
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) }] };
|
||||
},
|
||||
({id}) => runJsonTool(gatewayRequest(() => socket.config().getPrompt(id))),
|
||||
);
|
||||
|
||||
// ===================== Knowledge core tools =====================
|
||||
|
|
@ -377,11 +380,7 @@ export function createMcpServer(config: {
|
|||
"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) }] };
|
||||
},
|
||||
() => runJsonTool(gatewayRequest(() => socket.knowledge().getKnowledgeCores())),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -391,11 +390,7 @@ export function createMcpServer(config: {
|
|||
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) }] };
|
||||
},
|
||||
({id, collection}) => runJsonTool(gatewayRequest(() => socket.knowledge().deleteKgCore(id, collection))),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
|
|
@ -406,31 +401,42 @@ export function createMcpServer(config: {
|
|||
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);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(resp) }] };
|
||||
},
|
||||
({id, flow, collection}) => runJsonTool(gatewayRequest(() => socket.knowledge().loadKgCore(id, flow, collection))),
|
||||
);
|
||||
|
||||
return { server, socket };
|
||||
return {server, socket};
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
const { server, socket } = createMcpServer({
|
||||
gatewayUrl: process.env.GATEWAY_URL ?? "ws://localhost:8088/api/v1/rpc",
|
||||
user: process.env.USER_ID ?? "mcp",
|
||||
flowId: process.env.FLOW_ID ?? "default",
|
||||
...(process.env.GATEWAY_SECRET !== undefined
|
||||
? { token: process.env.GATEWAY_SECRET }
|
||||
: {}),
|
||||
});
|
||||
|
||||
export const runProgram = Effect.gen(function*() {
|
||||
const config = yield* loadTrustGraphMcpConfig();
|
||||
const serverConfig = {
|
||||
gatewayUrl: config.gatewayUrl,
|
||||
user: config.user,
|
||||
flowId: config.flowId,
|
||||
...(config.token === undefined ? {} : {token: config.token}),
|
||||
};
|
||||
const {server, socket} = createMcpServer(serverConfig);
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
socket.close();
|
||||
process.exit(0);
|
||||
yield* Effect.tryPromise({
|
||||
try: () => server.connect(transport),
|
||||
catch: stdioMcpError,
|
||||
});
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
process.on("SIGINT", () => {
|
||||
socket.close();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const stdioRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return stdioRuntime.runPromise(runProgram);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(runProgram);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue