From 709f6391b36208d414fda7111adcf3f6a84608be Mon Sep 17 00:00:00 2001 From: akitaSummer Date: Tue, 30 Dec 2025 03:14:07 +0800 Subject: [PATCH 1/3] feat: add structured tool --- .../src/decorator/GraphTool.ts | 2 + .../src/util/GraphToolInfoUtil.ts | 6 +++ core/mcp-client/package.json | 1 + core/mcp-client/src/HttpMCPClient.ts | 10 ++++ plugin/controller/test/http/request.test.ts | 2 +- .../langchain/lib/graph/GraphLoadUnitHook.ts | 47 ++++++++++++++++++- plugin/langchain/package.json | 3 +- .../modules/bar/controller/AppController.ts | 16 ++++++- plugin/langchain/test/llm.test.ts | 33 +++++++++++++ .../modules/bar/controller/AppController.ts | 16 +++++++ plugin/mcp-client/test/mcpclient.test.ts | 31 ++++++++++++ 11 files changed, 161 insertions(+), 6 deletions(-) diff --git a/core/langchain-decorator/src/decorator/GraphTool.ts b/core/langchain-decorator/src/decorator/GraphTool.ts index a3d24571..3388661e 100644 --- a/core/langchain-decorator/src/decorator/GraphTool.ts +++ b/core/langchain-decorator/src/decorator/GraphTool.ts @@ -28,3 +28,5 @@ export interface IGraphTool { execute: DynamicStructuredTool['func']; } +export type IGraphStructuredTool = DynamicStructuredTool[0]>; + diff --git a/core/langchain-decorator/src/util/GraphToolInfoUtil.ts b/core/langchain-decorator/src/util/GraphToolInfoUtil.ts index 25f433f2..327a4a43 100644 --- a/core/langchain-decorator/src/util/GraphToolInfoUtil.ts +++ b/core/langchain-decorator/src/util/GraphToolInfoUtil.ts @@ -4,11 +4,17 @@ import { GRAPH_TOOL_METADATA } from '../type/metadataKey'; import type { IGraphToolMetadata } from '../model/GraphToolMetadata'; export class GraphToolInfoUtil { + static graphToolMap = new Map(); static setGraphToolMetadata(metadata: IGraphToolMetadata, clazz: EggProtoImplClass) { MetadataUtil.defineMetaData(GRAPH_TOOL_METADATA, metadata, clazz); + GraphToolInfoUtil.graphToolMap.set(clazz, metadata); } static getGraphToolMetadata(clazz: EggProtoImplClass): IGraphToolMetadata | undefined { return MetadataUtil.getMetaData(GRAPH_TOOL_METADATA, clazz); } + + static getAllGraphToolMetadata(): Map { + return GraphToolInfoUtil.graphToolMap; + } } diff --git a/core/mcp-client/package.json b/core/mcp-client/package.json index 23bb93af..38d44fa2 100644 --- a/core/mcp-client/package.json +++ b/core/mcp-client/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.23.0", + "@langchain/mcp-adapters": "^1.0.0", "sdk-base": "^5.0.1", "urllib": "^4.6.11" }, diff --git a/core/mcp-client/src/HttpMCPClient.ts b/core/mcp-client/src/HttpMCPClient.ts index 567a40b3..f9ccb980 100644 --- a/core/mcp-client/src/HttpMCPClient.ts +++ b/core/mcp-client/src/HttpMCPClient.ts @@ -6,6 +6,7 @@ import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; import { fetch } from 'urllib'; import { mergeHeaders } from './HeaderUtil'; import type { Logger } from '@eggjs/tegg'; +import { loadMcpTools } from '@langchain/mcp-adapters'; export interface BaseHttpClientOptions extends ClientOptions { logger: Logger; fetch?: typeof fetch; @@ -30,12 +31,14 @@ export class HttpMCPClient extends Client { #transport: SSEClientTransport | StreamableHTTPClientTransport; #fetch: typeof fetch; url: string; + clientInfo: Implementation; constructor(clientInfo: Implementation, options: HttpClientOptions) { super(clientInfo, options); this.options = options; this.#fetch = options.fetch ?? fetch; this.logger = options.logger; this.url = options.url; + this.clientInfo = clientInfo; } async #buildSSESTransport() { const self = this; @@ -113,4 +116,11 @@ export class HttpMCPClient extends Client { } await this.connect(this.#transport, this.options.requestOptions); } + async getLangChainTool() { + return await loadMcpTools(this.clientInfo.name, this as any, { + throwOnLoadError: true, + prefixToolNameWithServerName: false, + additionalToolNamePrefix: '', + }); + } } diff --git a/plugin/controller/test/http/request.test.ts b/plugin/controller/test/http/request.test.ts index b3171ff5..d4701678 100644 --- a/plugin/controller/test/http/request.test.ts +++ b/plugin/controller/test/http/request.test.ts @@ -30,7 +30,7 @@ describe('plugin/controller/test/http/request.test.ts', () => { }); const [ nodeMajor ] = process.versions.node.split('.').map(v => Number(v)); if (nodeMajor >= 16) { - it.only('Request should work', async () => { + it('Request should work', async () => { app.mockCsrf(); const param = { name: 'foo', diff --git a/plugin/langchain/lib/graph/GraphLoadUnitHook.ts b/plugin/langchain/lib/graph/GraphLoadUnitHook.ts index 0a09259f..7c595a59 100644 --- a/plugin/langchain/lib/graph/GraphLoadUnitHook.ts +++ b/plugin/langchain/lib/graph/GraphLoadUnitHook.ts @@ -1,21 +1,30 @@ -import { EggProtoImplClass, LifecycleHook } from '@eggjs/tegg'; +import { AccessLevel, EggProtoImplClass, LifecycleHook, LifecyclePostInject, MCPInfoUtil, SingletonProto } from '@eggjs/tegg'; import { + ClassProtoDescriptor, + EggPrototypeCreatorFactory, EggPrototypeFactory, + EggPrototypeWithClazz, LoadUnit, LoadUnitLifecycleContext, + ProtoDescriptorHelper, } from '@eggjs/tegg-metadata'; import { CompiledStateGraphProto } from './CompiledStateGraphProto'; -import { GraphInfoUtil, IGraphMetadata } from '@eggjs/tegg-langchain-decorator'; +import { GraphInfoUtil, GraphToolInfoUtil, IGraphMetadata, IGraphTool, IGraphToolMetadata } from '@eggjs/tegg-langchain-decorator'; import assert from 'node:assert'; +import { EggContainerFactory } from '@eggjs/tegg-runtime'; +import { DynamicStructuredTool } from 'langchain'; +import * as z from 'zod/v4'; export class GraphLoadUnitHook implements LifecycleHook { private readonly eggPrototypeFactory: EggPrototypeFactory; clazzMap: Map; graphCompiledNameMap: Map = new Map(); + tools: Map; constructor(eggPrototypeFactory: EggPrototypeFactory) { this.eggPrototypeFactory = eggPrototypeFactory; this.clazzMap = GraphInfoUtil.getAllGraphMetadata(); + this.tools = GraphToolInfoUtil.getAllGraphToolMetadata(); } async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { @@ -30,7 +39,41 @@ export class GraphLoadUnitHook implements LifecycleHook).execute.bind(toolsObj.obj), + schema: z.object(ToolDetail.argsSchema) as any, + }); + Object.setPrototypeOf(this, tool); + } else { + throw new Error(`graph tool ${toolMeta.name ?? clazz.name} not found`); + } + } } + SingletonProto({ name: `structured${toolMeta.name ?? clazz.name}`, accessLevel: AccessLevel.PUBLIC })(StructuredTool); + return StructuredTool; } async postCreate(_ctx: LoadUnitLifecycleContext, obj: LoadUnit): Promise { diff --git a/plugin/langchain/package.json b/plugin/langchain/package.json index da6282f8..d5c5ca65 100644 --- a/plugin/langchain/package.json +++ b/plugin/langchain/package.json @@ -74,7 +74,8 @@ "koa-compose": "^3.2.1", "langchain": "^1.1.2", "sdk-base": "^4.2.0", - "urllib": "^4.4.0" + "urllib": "^4.4.0", + "zod": "^4.0.0" }, "devDependencies": { "@eggjs/module-test-util": "^3.68.0", diff --git a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts index 5c20bf76..836ec430 100644 --- a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts +++ b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts @@ -4,10 +4,10 @@ import { HTTPMethodEnum, Inject, } from '@eggjs/tegg'; -import { ChatModelQualifier, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator'; +import { ChatModelQualifier, IGraphStructuredTool, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator'; import { ChatOpenAIModel } from '../../../../../../../../lib/ChatOpenAI'; import { BoundChatModel } from '../service/BoundChatModel'; -import { FooGraph } from '../service/Graph'; +import { FooGraph, FooTool } from '../service/Graph'; import { AIMessage } from 'langchain'; @HTTPController({ @@ -24,6 +24,9 @@ export class AppController { @Inject() compiledFooGraph: TeggCompiledStateGraph; + @Inject() + structuredFooTool: IGraphStructuredTool; + @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello', @@ -59,4 +62,13 @@ export class AppController { }, ''), }; } + + @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/structured' }) + async structured() { + return { + name: this.structuredFooTool.name, + description: this.structuredFooTool.description, + schema: this.structuredFooTool.schema, + }; + } } diff --git a/plugin/langchain/test/llm.test.ts b/plugin/langchain/test/llm.test.ts index eca27953..fe1400a9 100644 --- a/plugin/langchain/test/llm.test.ts +++ b/plugin/langchain/test/llm.test.ts @@ -76,5 +76,38 @@ describe('plugin/langchain/test/llm.test.ts', () => { .get('/llm/graph') .expect(200, { value: 'hello graph toolhello world' }); }); + + it('should structured work', async () => { + const res = await app.httpRequest() + .get('/llm/structured') + .expect(200); + assert.deepStrictEqual(res.body, { + name: 'search', + description: 'Call the foo tool', + schema: { + '~standard': { + vendor: 'zod', + version: 1, + }, + def: { + type: 'object', + shape: { + query: { + '~standard': { + vendor: 'zod', + version: 1, + }, + def: { + type: 'string', + }, + format: null, + minLength: null, + maxLength: null, + }, + }, + }, + }, + }); + }); } }); diff --git a/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts b/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts index ce5322a8..177e7cdd 100644 --- a/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts +++ b/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts @@ -63,4 +63,20 @@ export class AppController { const res = await client.listTools(); return res; } + + @HTTPMethod({ + method: HTTPMethodEnum.GET, + path: '/hello-langchain-tools', + }) + async langchainTools() { + const tools = await this.mcpClient.getLangChainTool(); + return { + length: tools.length, + tools: tools.map(tool => ({ + name: tool.name, + description: tool.description, + schema: tool.schema, + })), + }; + } } diff --git a/plugin/mcp-client/test/mcpclient.test.ts b/plugin/mcp-client/test/mcpclient.test.ts index 946f0b0a..f0be0049 100644 --- a/plugin/mcp-client/test/mcpclient.test.ts +++ b/plugin/mcp-client/test/mcpclient.test.ts @@ -136,5 +136,36 @@ describe('plugin/mcp-client/test/mcpclient.test.ts', () => { ], }); }); + + it('should langchain tools work', async () => { + const res = await app.httpRequest() + .get('/mcpclient/hello-langchain-tools') + .expect(200); + assert.deepStrictEqual(res.body, { + length: 1, + tools: [ + { + name: 'add', + description: '', + schema: { + type: 'object', + properties: { + a: { + type: 'number', + }, + b: { + type: 'number', + }, + }, + required: [ + 'a', + 'b', + ], + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }); + }); } }); From 8ad457270a85458bc1334ec42970972d9ca60359 Mon Sep 17 00:00:00 2001 From: akitaSummer Date: Tue, 30 Dec 2025 14:16:23 +0800 Subject: [PATCH 2/3] fix: ci --- .../test/fixtures/apps/mcp-app/app/middleware/tracelog.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js b/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js index c3679a00..799e8203 100644 --- a/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js +++ b/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js @@ -3,6 +3,8 @@ module.exports = () => { return async function tracelog(ctx, next) { ctx.req.headers.trace = 'middleware'; + ctx.req.rawHeaders.push('trace'); + ctx.req.rawHeaders.push('middleware'); await next(); }; }; From 6c423f84f112e72dacc8ff1f43492def1536fa84 Mon Sep 17 00:00:00 2001 From: akitaSummer Date: Tue, 30 Dec 2025 14:31:03 +0800 Subject: [PATCH 3/3] fix: ci --- .../modules/bar/controller/AppController.ts | 1 - plugin/langchain/test/llm.test.ts | 23 ------------------- 2 files changed, 24 deletions(-) diff --git a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts index 836ec430..af48cab2 100644 --- a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts +++ b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts @@ -68,7 +68,6 @@ export class AppController { return { name: this.structuredFooTool.name, description: this.structuredFooTool.description, - schema: this.structuredFooTool.schema, }; } } diff --git a/plugin/langchain/test/llm.test.ts b/plugin/langchain/test/llm.test.ts index fe1400a9..31bc5caa 100644 --- a/plugin/langchain/test/llm.test.ts +++ b/plugin/langchain/test/llm.test.ts @@ -84,29 +84,6 @@ describe('plugin/langchain/test/llm.test.ts', () => { assert.deepStrictEqual(res.body, { name: 'search', description: 'Call the foo tool', - schema: { - '~standard': { - vendor: 'zod', - version: 1, - }, - def: { - type: 'object', - shape: { - query: { - '~standard': { - vendor: 'zod', - version: 1, - }, - def: { - type: 'string', - }, - format: null, - minLength: null, - maxLength: null, - }, - }, - }, - }, }); }); }