Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/langchain-decorator/src/decorator/GraphTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ export interface IGraphTool<ToolSchema = ToolSchemaBase> {
execute: DynamicStructuredTool<ToolSchema>['func'];
}

export type IGraphStructuredTool<T extends IGraphTool> = DynamicStructuredTool<Parameters<T['execute']>[0]>;

6 changes: 6 additions & 0 deletions core/langchain-decorator/src/util/GraphToolInfoUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { GRAPH_TOOL_METADATA } from '../type/metadataKey';
import type { IGraphToolMetadata } from '../model/GraphToolMetadata';

export class GraphToolInfoUtil {
static graphToolMap = new Map<EggProtoImplClass, IGraphToolMetadata>();
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<EggProtoImplClass, IGraphToolMetadata> {
return GraphToolInfoUtil.graphToolMap;
}
}
1 change: 1 addition & 0 deletions core/mcp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"@langchain/mcp-adapters": "^1.0.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@langchain/mcp-adapters npm package version 1.0.0

💡 Result:

I checked the package on official npm and CDN sources. The npm package @langchain/mcp-adapters is published (current visible npm release listed as 0.6.0 on npmjs.com) and the LangChain docs recommend installing @langchain/mcp-adapters. I could not find an official npm release page showing a 1.0.0 tag; jsDelivr shows higher CDN versions (e.g., 1.1.1) which may reflect mirrored/packaged builds rather than the npm registry listing. Sources: [npmjs package page][1], [LangChain docs][2], [jsDelivr version listing][3].

If you want, I can fetch the npm registry metadata directly to list all published versions.

Sources:
[1] https://www.npmjs.com/package/%40langchain/mcp-adapters
[2] https://docs.langchain.com/oss/javascript/langchain/mcp
[3] https://www.jsdelivr.com/package/npm/%40langchain/mcp-adapters


Fix version constraint: @langchain/mcp-adapters version 1.0.0 does not exist on npm.

The latest published version on npm registry is 0.6.0. The current constraint ^1.0.0 will fail during installation. Update to a published version (e.g., ^0.6.0 or later if available).

🤖 Prompt for AI Agents
In core/mcp-client/package.json around line 40, the dependency version for
"@langchain/mcp-adapters" is set to "^1.0.0" which does not exist on npm; change
the version to a published release (for example "^0.6.0" or the latest
available) in the dependencies list so installations succeed, then run npm
install (or yarn) and update lockfile if present.

"sdk-base": "^5.0.1",
"urllib": "^4.6.11"
},
Expand Down
10 changes: 10 additions & 0 deletions core/mcp-client/src/HttpMCPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of this as any bypasses TypeScript's type checking and can hide underlying type incompatibility issues. The HttpMCPClient class extends Client, which is likely the type expected by loadMcpTools. If there is a type mismatch causing this cast to be necessary, it should be addressed directly to ensure type safety and prevent potential runtime errors. Please either fix the type incompatibility or add a comment explaining why this cast is unavoidable.

throwOnLoadError: true,
prefixToolNameWithServerName: false,
additionalToolNamePrefix: '',
});
}
Comment on lines +119 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find where loadMcpTools is defined and its signature
rg -n 'export.*loadMcpTools|function loadMcpTools|const loadMcpTools' --type=ts --type=tsx -A 15

Repository: eggjs/tegg

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Find all usages of getLangChainTool
rg -n 'getLangChainTool' --type=ts --type=tsx -B 2 -A 2

Repository: eggjs/tegg

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Check the HttpMCPClient class definition and see what interface it implements
rg -n 'class HttpMCPClient|interface.*HttpMCPClient' --type=ts --type=tsx -A 20

Repository: eggjs/tegg

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Look for the interface that loadMcpTools expects
rg -n 'MCPClient|MCPClientI' --type=ts --type=tsx | head -30

Repository: eggjs/tegg

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Find where loadMcpTools is defined and its signature
rg -n 'export.*loadMcpTools|function loadMcpTools|const loadMcpTools' --type=ts -A 15

Repository: eggjs/tegg

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Find all usages of getLangChainTool
rg -n 'getLangChainTool' --type=ts -B 2 -A 2

Repository: eggjs/tegg

Length of output: 1063


🏁 Script executed:

#!/bin/bash
# Check the HttpMCPClient class definition
rg -n 'class HttpMCPClient' --type=ts -A 30

Repository: eggjs/tegg

Length of output: 3642


🏁 Script executed:

#!/bin/bash
# Search for MCPClient interface definitions
rg -n 'interface.*MCPClient|type.*MCPClient' --type=ts | head -20

Repository: eggjs/tegg

Length of output: 141


🏁 Script executed:

#!/bin/bash
# Find the loadMcpTools function definition
fd -t f -e ts . | xargs grep -l 'loadMcpTools' | head -10

Repository: eggjs/tegg

Length of output: 192


🏁 Script executed:

#!/bin/bash
# Get the full file path and context around loadMcpTools definition
rg -n 'loadMcpTools' --type=ts -B 5 -A 20 | head -100

Repository: eggjs/tegg

Length of output: 9429


🏁 Script executed:

#!/bin/bash
# Check what Client base class HttpMCPClient extends
rg -n "from.*Client|import.*Client" core/mcp-client/src/HttpMCPClient.ts

Repository: eggjs/tegg

Length of output: 385


🏁 Script executed:

#!/bin/bash
# Check the return value of loadMcpTools in the usage
cat core/mcp-client/src/HttpMCPClient.ts | head -130 | tail -20

Repository: eggjs/tegg

Length of output: 624


🏁 Script executed:

#!/bin/bash
# Check package.json to see the version of @langchain/mcp-adapters
find . -name "package.json" -type f | xargs grep -l "mcp-adapters" 2>/dev/null

Repository: eggjs/tegg

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Look at the actual usage in test files to understand the return type better
cat plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts | head -80 | tail -20

Repository: eggjs/tegg

Length of output: 510


🏁 Script executed:

#!/bin/bash
# Check if there are any type definitions or imports related to MCPClient interface
rg -n "MCPClient|interface.*Client" --type=ts core/mcp-client/src/ -A 5 | head -50

Repository: eggjs/tegg

Length of output: 4012


🏁 Script executed:

#!/bin/bash
# Verify the complete file at HttpMCPClient.ts to ensure we see the full context
wc -l core/mcp-client/src/HttpMCPClient.ts

Repository: eggjs/tegg

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Check if there are any other callers of getLangChainTool besides the one in AppController
rg -n 'getLangChainTool' --type=ts

Repository: eggjs/tegg

Length of output: 276


Rename method and remove type bypass to improve naming consistency and type safety.

Three issues need to be addressed:

  1. Naming inconsistency: Method is named getLangChainTool (singular) but returns an array of tools, confirmed by the test usage that calls .length and .map() on the result. Rename to getLangChainTools.

  2. Type safety bypass: Line 120 uses this as any, circumventing TypeScript's type checking. Remove the cast to rely on proper type compatibility.

  3. Missing documentation: Add JSDoc to explain the return type and behavior.

The method has one caller in plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts:72 that will need updating when renamed.

🤖 Prompt for AI Agents
In core/mcp-client/src/HttpMCPClient.ts around lines 119 to 125, rename the
method from getLangChainTool to getLangChainTools (plural) because it returns an
array of tools, remove the unsafe cast "this as any" so the call uses the proper
typed this (adjust the HttpMCPClient/loader types if necessary to satisfy
TypeScript), and add a concise JSDoc above the method describing that it returns
Promise<Array<...>> of loaded MCP tools and the options used; also update the
sole caller at
plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts:72
to call getLangChainTools().

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
};
2 changes: 1 addition & 1 deletion plugin/controller/test/http/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
47 changes: 45 additions & 2 deletions plugin/langchain/lib/graph/GraphLoadUnitHook.ts
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The import from zod/v4 is highly unusual. The latest major version of the public zod package is v3, and it does not provide a v4 entry point. While the package.json specifies "zod": "^4.0.0", this version does not exist on the public npm registry, suggesting a private fork or a typo. This can cause confusion and build issues for other developers. Please change the import to import * as z from 'zod'; and adjust the dependency version if this is a mistake. If it's intentional, please add a comment explaining the reason for this specific import path.


export class GraphLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
private readonly eggPrototypeFactory: EggPrototypeFactory;
clazzMap: Map<EggProtoImplClass, IGraphMetadata>;
graphCompiledNameMap: Map<string, CompiledStateGraphProto> = new Map();
tools: Map<EggProtoImplClass, IGraphToolMetadata>;

constructor(eggPrototypeFactory: EggPrototypeFactory) {
this.eggPrototypeFactory = eggPrototypeFactory;
this.clazzMap = GraphInfoUtil.getAllGraphMetadata();
this.tools = GraphToolInfoUtil.getAllGraphToolMetadata();
}

async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
Expand All @@ -30,7 +39,41 @@ export class GraphLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext
this.eggPrototypeFactory.registerPrototype(proto, loadUnit);
this.graphCompiledNameMap.set(protoName, proto);
}
const toolMeta = this.tools.get(clazz as EggProtoImplClass);
if (toolMeta) {
const StructuredTool = this.createStructuredTool(clazz, toolMeta);
const protoDescriptor = ProtoDescriptorHelper.createByInstanceClazz(StructuredTool, {
moduleName: loadUnit.name,
unitPath: loadUnit.unitPath,
}) as ClassProtoDescriptor;
const proto = await EggPrototypeCreatorFactory.createProtoByDescriptor(protoDescriptor, loadUnit);
this.eggPrototypeFactory.registerPrototype(proto, loadUnit);
}
}
}

createStructuredTool(clazz: EggProtoImplClass, toolMeta: IGraphToolMetadata) {
class StructuredTool {
@LifecyclePostInject()
async init() {
const toolsObj = await EggContainerFactory.getOrCreateEggObjectFromClazz(clazz);
const toolMetadata = GraphToolInfoUtil.getGraphToolMetadata((toolsObj.proto as unknown as EggPrototypeWithClazz).clazz!);
const ToolDetail = MCPInfoUtil.getMCPToolArgsIndex((toolsObj.proto as unknown as EggPrototypeWithClazz).clazz!, 'execute');
Comment on lines +60 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The expression (toolsObj.proto as unknown as EggPrototypeWithClazz).clazz! is repeated and uses multiple unsafe type assertions (as unknown as, !). This reduces code readability and introduces a risk of runtime errors if clazz is not present on the prototype. It would be safer and clearer to extract the class into a variable with a proper null check before using it.

        const toolClazz = (toolsObj.proto as EggPrototypeWithClazz).clazz;
        if (!toolClazz) {
          throw new Error(`Could not find class for prototype '${toolsObj.proto.name}'`);
        }
        const toolMetadata = GraphToolInfoUtil.getGraphToolMetadata(toolClazz);
        const ToolDetail = MCPInfoUtil.getMCPToolArgsIndex(toolClazz, 'execute');

if (toolMetadata && ToolDetail) {
const tool = new DynamicStructuredTool({
description: toolMetadata.description,
name: toolMetadata.toolName,
func: (toolsObj.obj as unknown as IGraphTool<any>).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;
}
Comment on lines +55 to 77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent property access: toolMeta.name vs toolMeta.toolName.

Based on the IGraphToolMetadata interface (from the relevant code snippets), the property is toolName, not name. Lines 71 and 75 use toolMeta.name which may be undefined or incorrect.

🔎 Proposed fix
         } else {
-          throw new Error(`graph tool ${toolMeta.name ?? clazz.name} not found`);
+          throw new Error(`graph tool ${toolMeta.toolName ?? clazz.name} not found`);
         }
       }
     }
-    SingletonProto({ name: `structured${toolMeta.name ?? clazz.name}`, accessLevel: AccessLevel.PUBLIC })(StructuredTool);
+    SingletonProto({ name: `structured${toolMeta.toolName ?? clazz.name}`, accessLevel: AccessLevel.PUBLIC })(StructuredTool);
     return StructuredTool;
🤖 Prompt for AI Agents
In plugin/langchain/lib/graph/GraphLoadUnitHook.ts around lines 55 to 77, the
code inconsistently accesses toolMeta.name but the IGraphToolMetadata uses
toolName; replace both occurrences of toolMeta.name with toolMeta.toolName
(keeping the existing fallback to clazz.name) so the error message and
SingletonProto name use the correct metadata property.


async postCreate(_ctx: LoadUnitLifecycleContext, obj: LoadUnit): Promise<void> {
Expand Down
3 changes: 2 additions & 1 deletion plugin/langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -24,6 +24,9 @@ export class AppController {
@Inject()
compiledFooGraph: TeggCompiledStateGraph<FooGraph>;

@Inject()
structuredFooTool: IGraphStructuredTool<FooTool>;

@HTTPMethod({
method: HTTPMethodEnum.GET,
path: '/hello',
Expand Down Expand Up @@ -59,4 +62,12 @@ export class AppController {
}, ''),
};
}

@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/structured' })
async structured() {
return {
name: this.structuredFooTool.name,
description: this.structuredFooTool.description,
};
}
}
10 changes: 10 additions & 0 deletions plugin/langchain/test/llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,15 @@ 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',
});
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
};
}
}
31 changes: 31 additions & 0 deletions plugin/mcp-client/test/mcpclient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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#',
},
},
],
});
});
}
});
Loading