Repository: AIGNE-io/aigne-framework
Pull request #54: feat: add `serve` command for @aigne/cli
Author: login: li-yechao
Created: 2025-04-07T08:14:32Z | Merged: 2025-04-08T00:44:28Z

## Related Issue

<!-- Use keywords like fixes, closes, resolves, relates to link the issue. In principle, all PRs should be associated with an issue -->

### Major Changes
1. add serve command for @aigne/cli

### Checklist

- [x] This change requires documentation updates, and I have updated the relevant documentation. If the documentation has not been updated, please create a documentation update issue and link it here
- [x] The changes are already covered by tests, and I have adjusted the test coverage for the changed parts
- [x] The newly added code logic is also covered by tests
- [x] This change adds dependencies, and they are placed in dependencies and devDependencies
- [x] This change includes adding or updating npm dependencies, and it does not result in multiple versions of the same dependency [check the diff of pnpm-lock.yaml]

<!-- This is an auto-generated comment: release notes by AIGNE Reviewer -->
### Summary by AIGNE

Release Notes:

- New Feature: Added metadata support (name and description) to Execution Engine, enabling better identification and documentation of execution configurations
- Improvement: Enhanced type safety in Agent configuration system for more reliable development experience
- Chore: Streamlined test infrastructure by removing debug logging
- Refactor: Improved TypeScript configuration for better module resolution in CLI

These changes primarily enhance the developer experience and lay groundwork for better execution engine identification and configuration. End users will benefit from improved metadata support when working with execution configurations.
<!-- end of auto-generated comment: release notes by AIGNE Reviewer -->


Diff:
diff --git a/docs/cli.md b/docs/cli.md
index 1772cf4da..2aab1d5b0 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -126,6 +126,38 @@ Run tests in a specific directory:
 aigne test ./my-agents
 ```
 
+### `aigne serve`
+
+Serve the agents in the specified directory as a Model Context Protocol (MCP) server.
+
+```bash
+aigne serve [path] [options]
+```
+
+#### Arguments
+
+- `path`: Path to the agents directory (defaults to current directory `.`)
+
+#### Options
+
+- `--mcp`: Serve the agents as a MCP server (required at this time)
+- `--port <port>`: Port to run the MCP server on (defaults to 3000)
+- `--help`: Display help for the command
+
+#### Examples
+
+Serve agents in the current directory as an MCP server on the default port:
+
+```bash
+aigne serve --mcp
+```
+
+Serve agents in a specific directory with a custom port:
+
+```bash
+aigne serve ./my-agents --mcp --port 8080
+```
+
 ## Usage Examples
 
 ### Create and Run an Agent
diff --git a/docs/cli.zh.md b/docs/cli.zh.md
index 7eab95682..119fa5ae6 100644
--- a/docs/cli.zh.md
+++ b/docs/cli.zh.md
@@ -126,6 +126,38 @@ aigne test
 aigne test ./my-agents
 ```
 
+### `aigne serve`
+
+将指定目录中的代理作为模型上下文协议（MCP）服务器提供。
+
+```bash
+aigne serve [路径] [选项]
+```
+
+#### 参数
+
+- `路径`：代理目录的路径（默认为当前目录 `.`）
+
+#### 选项
+
+- `--mcp`：将代理作为 MCP 服务器提供（目前为必需选项）
+- `--port <端口>`：运行 MCP 服务器的端口（默认为 3000）
+- `--help`：显示命令帮助
+
+#### 示例
+
+在默认端口上将当前目录中的代理作为 MCP 服务器提供：
+
+```bash
+aigne serve --mcp
+```
+
+将特定目录中的代理与自定义端口一起提供：
+
+```bash
+aigne serve ./my-agents --mcp --port 8080
+```
+
 ## 使用示例
 
 ### 创建并运行代理
diff --git a/packages/agent-library/package.json b/packages/agent-library/package.json
index 7e4bc5967..96625a3de 100644
--- a/packages/agent-library/package.json
+++ b/packages/agent-library/package.json
@@ -43,8 +43,8 @@
     "build": "tsc --build scripts/tsconfig.build.json",
     "clean": "rimraf lib test/coverage",
     "prepublishOnly": "run-s clean build",
-    "test": "cd test && bun test",
-    "test:coverage": "cd test && bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
+    "test": "bun --cwd test test",
+    "test:coverage": "bun --cwd test test --coverage --coverage-reporter=lcov --coverage-reporter=text",
     "postbuild": "echo '{\"type\": \"module\"}' > lib/esm/package.json && echo '{\"type\": \"commonjs\"}' > lib/cjs/package.json"
   },
   "dependencies": {
diff --git a/packages/agent-library/test/tsconfig.json b/packages/agent-library/test/tsconfig.json
new file mode 100644
index 000000000..83da9f820
--- /dev/null
+++ b/packages/agent-library/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "paths": {
+      "@aigne/agent-library/*": ["../src/*"]
+    }
+  }
+}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 0f1a48c7a..393d973db 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -19,10 +19,20 @@
     "CHANGELOG.md",
     "templates"
   ],
-  "type": "module",
   "bin": {
     "aigne": "dist/cli.js"
   },
+  "type": "module",
+  "exports": {
+    "./*": "./dist/*"
+  },
+  "typesVersions": {
+    "*": {
+      "*": [
+        "./dist/*"
+      ]
+    }
+  },
   "scripts": {
     "lint": "tsc --noEmit",
     "build": "tsc --build tsconfig.build.json",
@@ -30,26 +40,32 @@
     "prepublishOnly": "run-s clean build",
     "test": "run-s test:src test:templates",
     "test:coverage": "run-s test:src:coverage test:templates:coverage",
-    "test:src": "cd test && bun test",
-    "test:src:coverage": "cd test && bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
+    "test:src": "bun --cwd test test",
+    "test:src:coverage": "bun --cwd test test --coverage --coverage-reporter=lcov --coverage-reporter=text",
     "test:templates": "cd templates && node --test",
     "test:templates:coverage": "cd templates && mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-reporter=spec --test-reporter-destination=stdout"
   },
   "dependencies": {
     "@aigne/core": "workspace:^",
+    "@modelcontextprotocol/sdk": "^1.8.0",
     "chalk": "^5.4.1",
     "commander": "^13.1.0",
+    "express": "^5.1.0",
     "gradient-string": "^3.0.0",
     "inquirer": "^12.5.0",
-    "openai": "^4.91.1"
+    "openai": "^4.91.1",
+    "zod": "^3.24.2"
   },
   "devDependencies": {
     "@aigne/test-utils": "workspace:^",
     "@types/bun": "^1.2.8",
+    "@types/express": "^5.0.1",
     "@types/gradient-string": "^1.1.6",
     "@types/node": "^22.14.0",
+    "detect-port": "^2.1.0",
     "npm-run-all": "^4.1.5",
     "rimraf": "^6.0.1",
-    "typescript": "^5.8.2"
+    "typescript": "^5.8.2",
+    "ufo": "^1.6.0"
   }
 }
diff --git a/packages/cli/src/commands/aigne.ts b/packages/cli/src/commands/aigne.ts
index 41df28f39..88ee8329c 100644
--- a/packages/cli/src/commands/aigne.ts
+++ b/packages/cli/src/commands/aigne.ts
@@ -3,6 +3,7 @@ import pkg from "../../package.json" with { type: "json" };
 import { asciiLogo } from "../utils/ascii-logo.js";
 import { createCreateCommand } from "./create.js";
 import { createRunCommand } from "./run.js";
+import { createServeCommand } from "./serve.js";
 import { createTestCommand } from "./test.js";
 
 export function createAIGNECommand(): Command {
@@ -15,6 +16,7 @@ export function createAIGNECommand(): Command {
     .addCommand(createRunCommand())
     .addCommand(createTestCommand())
     .addCommand(createCreateCommand())
+    .addCommand(createServeCommand())
     .showHelpAfterError(true)
     .showSuggestionAfterError(true);
 }
diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts
new file mode 100644
index 000000000..cacdec08d
--- /dev/null
+++ b/packages/cli/src/commands/serve.ts
@@ -0,0 +1,41 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { tryOrThrow } from "@aigne/core/utils/type-utils.js";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port?: number;
+  mcp?: boolean;
+}
+
+const DEFAULT_PORT = () =>
+  tryOrThrow(
+    () => {
+      const { PORT } = process.env;
+      if (!PORT) return 3000;
+      const port = Number.parseInt(PORT);
+      if (!port || !Number.isInteger(port)) throw new Error(`Invalid PORT: ${PORT}`);
+      return port;
+    },
+    (error) => new Error(`parse PORT error ${error.message}`),
+  );
+
+export function createServeCommand(): Command {
+  return new Command("serve")
+    .description("Serve the agents in the specified directory as a MCP server")
+    .argument("[path]", "Path to the agents directory", ".")
+    .option("--mcp", "Serve the agents as a MCP server")
+    .option("--port <port>", "Port to run the MCP server on", (s) => Number.parseInt(s))
+    .action(async (path: string, options: ServeMCPOptions) => {
+      const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
+      const port = options.port || DEFAULT_PORT();
+
+      const engine = await ExecutionEngine.load({ path: absolutePath });
+
+      if (options.mcp) await serveMCPServer({ engine, port });
+      else throw new Error("Default server is not supported yet. Please use --mcp option");
+    })
+    .showHelpAfterError(true)
+    .showSuggestionAfterError(true);
+}
diff --git a/packages/cli/src/utils/serve-mcp.ts b/packages/cli/src/utils/serve-mcp.ts
new file mode 100644
index 000000000..80a4202c4
--- /dev/null
+++ b/packages/cli/src/utils/serve-mcp.ts
@@ -0,0 +1,87 @@
+import { type ExecutionEngine, getMessage } from "@aigne/core";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import express, { type ErrorRequestHandler, type Request, type Response } from "express";
+import { ZodObject, type ZodRawShape } from "zod";
+
+export async function serveMCPServer({ engine, port }: { engine: ExecutionEngine; port: number }) {
+  const server = new McpServer(
+    {
+      name: engine.name || "aigne-mcp-server",
+      version: "1.0.0",
+    },
+    {
+      capabilities: { tools: {} },
+      instructions: engine.description,
+    },
+  );
+
+  for (const agent of engine.agents) {
+    const schema = agent.inputSchema;
+
+    if (!(schema instanceof ZodObject)) throw new Error("Agent input schema must be a ZodObject");
+
+    server.tool(agent.name, agent.description || "", schema.shape as ZodRawShape, async (input) => {
+      const result = await engine.call(agent, input);
+
+      return {
+        content: [
+          {
+            type: "text",
+            text: getMessage(result) || JSON.stringify(result),
+          },
+        ],
+      };
+    });
+  }
+
+  const app = express();
+
+  const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+  app.get("/sse", async (req: Request, res: Response) => {
+    const transport = new SSEServerTransport("/messages", res);
+    transports[transport.sessionId] = transport;
+    req.on("close", () => {
+      delete transports[transport.sessionId];
+    });
+    await server.connect(transport);
+  });
+
+  app.post("/messages", async (req: Request, res: Response) => {
+    const sessionId = req.query.sessionId as string;
+    const transport = transports[sessionId];
+    if (transport) {
+      await transport.handlePostMessage(req, res);
+    } else {
+      throw new HttpError(400, "No transport found for sessionId");
+    }
+  });
+
+  app.use(<ErrorRequestHandler>((error, _req, res, _next) => {
+    console.error("handle route error", { error });
+    res
+      .status(error instanceof HttpError ? error.status : 500)
+      .json({ error: { message: error.message } });
+  }));
+
+  const { promise, resolve, reject } = Promise.withResolvers();
+
+  const httpServer = app.listen(port, (error) => {
+    if (error) reject(error);
+    resolve();
+  });
+
+  await promise;
+
+  return httpServer;
+}
+
+class HttpError extends Error {
+  constructor(
+    public status: number,
+    message: string,
+  ) {
+    super(message);
+  }
+}
diff --git a/packages/cli/test-agents/aigne.yaml b/packages/cli/test-agents/aigne.yaml
index fd8cd8587..8b6f01d02 100644
--- a/packages/cli/test-agents/aigne.yaml
+++ b/packages/cli/test-agents/aigne.yaml
@@ -1,3 +1,5 @@
+name: test_aigne_project
+description: A test project for the aigne agent
 chat_model:
   name: gpt-4o-mini
   temperature: 0.8
diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts
index bf562583d..8fe65a7df 100644
--- a/packages/cli/test/cli.test.ts
+++ b/packages/cli/test/cli.test.ts
@@ -4,11 +4,11 @@ import { Command } from "commander";
 test("CLI imports and loads correctly", async () => {
   const createAIGNECommand = mock(() => new Command());
 
-  mock.module("../src/commands/aigne.js", () => ({
+  mock.module("@aigne/cli/commands/aigne.js", () => ({
     createAIGNECommand,
   }));
 
-  await import("../src/cli.js");
+  await import("@aigne/cli/cli.js");
 
   expect(createAIGNECommand).toHaveBeenCalled();
 });
diff --git a/packages/cli/test/commands/aigne.test.ts b/packages/cli/test/commands/aigne.test.ts
index 3e4a0face..135f42a25 100644
--- a/packages/cli/test/commands/aigne.test.ts
+++ b/packages/cli/test/commands/aigne.test.ts
@@ -1,6 +1,6 @@
 import { expect, test } from "bun:test";
+import { createAIGNECommand } from "@aigne/cli/commands/aigne.js";
 import { Command } from "commander";
-import { createAIGNECommand } from "../../src/commands/aigne.js";
 
 test("aigne command should parse --version correctly", async () => {
   const command = createAIGNECommand();
diff --git a/packages/cli/test/commands/run.test.ts b/packages/cli/test/commands/run.test.ts
index ff2906e71..de872b53e 100644
--- a/packages/cli/test/commands/run.test.ts
+++ b/packages/cli/test/commands/run.test.ts
@@ -1,8 +1,8 @@
-import { expect, mock, test } from "bun:test";
+import { expect, mock, spyOn, test } from "bun:test";
 import { join, relative } from "node:path";
+import { createRunCommand } from "@aigne/cli/commands/run.js";
 import { UserAgent } from "@aigne/core";
 import { mockModule } from "@aigne/test-utils/mock-module.js";
-import { createRunCommand } from "../../src/commands/run.js";
 
 test("run command should call run chat loop correctly", async () => {
   const runChatLoopInTerminal = mock();
@@ -52,6 +52,7 @@ test("run command should call run chat loop correctly", async () => {
   );
 
   // should error if agent not found
+  spyOn(console, "error").mockReturnValueOnce(undefined);
   expect(command.parseAsync(["", "run", testAgentsPath, "--agent", "chat1"])).rejects.toThrow(
     "not found",
   );
diff --git a/packages/cli/test/commands/serve.test.ts b/packages/cli/test/commands/serve.test.ts
new file mode 100644
index 000000000..4947b3808
--- /dev/null
+++ b/packages/cli/test/commands/serve.test.ts
@@ -0,0 +1,84 @@
+import { expect, mock, test } from "bun:test";
+import { join } from "node:path";
+import { createServeCommand } from "@aigne/cli/commands/serve.js";
+import { mockModule } from "@aigne/test-utils/mock-module.js";
+import { detect } from "detect-port";
+
+test("serve-mcp command should work with default options", async () => {
+  const mockListen = mock().mockImplementationOnce((_, cb) => cb());
+  const mockExpress = mock(() => ({
+    use: mock(),
+    get: mock(),
+    post: mock(),
+    listen: mockListen,
+  }));
+
+  await using _ = await mockModule("express", () => ({
+    default: mockExpress,
+  }));
+
+  const command = createServeCommand();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+
+  const cwd = process.cwd();
+  process.chdir(testAgentsPath);
+  await command.parseAsync(["", "serve", "--mcp"]);
+  expect(mockListen).toHaveBeenCalledWith(3000, expect.any(Function));
+  process.chdir(cwd);
+});
+
+test("serve-mcp command should work with custom options", async () => {
+  const mockListen = mock().mockImplementationOnce((_, cb) => cb());
+  const mockExpress = mock(() => ({
+    use: mock(),
+    get: mock(),
+    post: mock(),
+    listen: mockListen,
+  }));
+
+  await using _ = await mockModule("express", () => ({
+    default: mockExpress,
+  }));
+
+  const port = await detect();
+
+  const command = createServeCommand();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+
+  await command.parseAsync(["", "serve", "--mcp", "--port", port.toString(), testAgentsPath]);
+
+  expect(mockListen).toHaveBeenLastCalledWith(port, expect.any(Function));
+});
+
+test("serve-mcp command should use process.env.PORT", async () => {
+  const mockListen = mock().mockImplementationOnce((_, cb) => cb());
+  const mockExpress = mock(() => ({
+    use: mock(),
+    get: mock(),
+    post: mock(),
+    listen: mockListen,
+  }));
+
+  await using _ = await mockModule("express", () => ({
+    default: mockExpress,
+  }));
+
+  const port = await detect();
+
+  const command = createServeCommand();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+
+  process.env.PORT = port.toString();
+
+  await command.parseAsync(["", "serve", "--mcp", testAgentsPath]);
+
+  expect(mockListen).toHaveBeenLastCalledWith(port, expect.any(Function));
+
+  process.env.PORT = "INVALID_PORT";
+  expect(command.parseAsync(["", "serve", "--mcp", testAgentsPath])).rejects.toThrow(
+    "parse PORT error",
+  );
+});
diff --git a/packages/cli/test/commands/test.test.ts b/packages/cli/test/commands/test.test.ts
index 6c882cc6a..335bf1142 100644
--- a/packages/cli/test/commands/test.test.ts
+++ b/packages/cli/test/commands/test.test.ts
@@ -1,7 +1,7 @@
 import { expect, spyOn, test } from "bun:test";
 import * as childProcess from "node:child_process";
 import { join } from "node:path";
-import { createTestCommand } from "../../src/commands/test.js";
+import { createTestCommand } from "@aigne/cli/commands/test.js";
 
 test("test command should spawn node --test process with correct arguments", async () => {
   const spawnSyncMock = spyOn(childProcess, "spawnSync").mockReturnValue(
diff --git a/packages/cli/test/tsconfig.json b/packages/cli/test/tsconfig.json
new file mode 100644
index 000000000..0b8d3567f
--- /dev/null
+++ b/packages/cli/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "paths": {
+      "@aigne/cli/*": ["../src/*"]
+    }
+  }
+}
diff --git a/packages/cli/test/utils/serve-mcp.test.ts b/packages/cli/test/utils/serve-mcp.test.ts
new file mode 100644
index 000000000..79d7da3b1
--- /dev/null
+++ b/packages/cli/test/utils/serve-mcp.test.ts
@@ -0,0 +1,74 @@
+import { expect, spyOn, test } from "bun:test";
+import assert from "node:assert";
+import { join } from "node:path";
+import { serveMCPServer } from "@aigne/cli/utils/serve-mcp.js";
+import { ExecutionEngine } from "@aigne/core";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { detect } from "detect-port";
+import { withQuery } from "ufo";
+
+test("serveMCPServer should work", async () => {
+  const port = await detect();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+  const engine = await ExecutionEngine.load({ path: testAgentsPath });
+
+  assert(engine.model, "engine.model should be defined");
+  spyOn(engine.model, "call")
+    .mockReturnValueOnce(
+      Promise.resolve({
+        text: "hello, how can I help you?",
+      }),
+    )
+    .mockImplementation(() => Promise.reject(new Error("not implemented")));
+
+  const server = await serveMCPServer({ engine, port });
+
+  const url = `http://localhost:${port}/sse`;
+  const transport = new SSEClientTransport(new URL(url));
+  const client = new Client({
+    name: "test-client",
+    version: "1.0.0",
+  });
+
+  await client.connect(transport);
+  expect(client.getServerVersion()).toEqual({ name: "test_aigne_project", version: "1.0.0" });
+
+  expect(client.listTools()).resolves.toEqual({
+    tools: [
+      expect.objectContaining({
+        name: "chat",
+      }),
+    ],
+  });
+
+  expect(client.callTool({ name: "chat", arguments: { $message: "hello" } })).resolves.toEqual({
+    content: [{ type: "text", text: "hello, how can I help you?" }],
+  });
+
+  await client.close();
+  server.closeAllConnections();
+  server.close();
+});
+
+test("serveMCPServer should respond error by express router", async () => {
+  const port = await detect();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+  const engine = await ExecutionEngine.load({ path: testAgentsPath });
+  const server = await serveMCPServer({ engine, port });
+
+  spyOn(console, "error").mockReturnValueOnce(undefined);
+  const result = await fetch(
+    withQuery(`http://localhost:${port}/messages`, { sessionId: "test-session-id" }),
+    { method: "POST" },
+  );
+  expect(result.status).toBe(400);
+  expect(result.json()).resolves.toEqual({
+    error: { message: "No transport found for sessionId" },
+  });
+
+  server.closeAllConnections();
+  server.close();
+});
diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json
index 9cdeb6a73..08d91db6d 100644
--- a/packages/cli/tsconfig.build.json
+++ b/packages/cli/tsconfig.build.json
@@ -7,6 +7,7 @@
     "outDir": "./dist",
     "rootDir": "./src",
     "noEmit": false,
+    "declaration": true,
     "paths": {}
   },
   "include": ["./src"]
diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json
index e4ff3a007..9d49a0a26 100644
--- a/packages/cli/tsconfig.json
+++ b/packages/cli/tsconfig.json
@@ -3,7 +3,8 @@
   "compilerOptions": {
     "paths": {
       "@aigne/core": ["../core/src"],
-      "@aigne/core/*": ["../core/src/*"]
+      "@aigne/core/*": ["../core/src/*"],
+      "@aigne/cli/*": ["./src/*"]
     }
   },
   "exclude": ["node_modules", "dist"],
diff --git a/packages/core/package.json b/packages/core/package.json
index 0439a130c..e029b9894 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -63,8 +63,8 @@
     "build": "tsc --build scripts/tsconfig.build.json",
     "clean": "rimraf lib test/coverage",
     "prepublishOnly": "run-s clean build",
-    "test": "cd test && bun test",
-    "test:coverage": "cd test && bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
+    "test": "bun --cwd test test",
+    "test:coverage": "bun --cwd test test --coverage --coverage-reporter=lcov --coverage-reporter=text",
     "postbuild": "echo '{\"type\": \"module\"}' > lib/esm/package.json && echo '{\"type\": \"commonjs\"}' > lib/cjs/package.json"
   },
   "dependencies": {
diff --git a/packages/core/src/execution-engine/execution-engine.ts b/packages/core/src/execution-engine/execution-engine.ts
index 85e3d12e1..894214d5d 100644
--- a/packages/core/src/execution-engine/execution-engine.ts
+++ b/packages/core/src/execution-engine/execution-engine.ts
@@ -15,6 +15,8 @@ import {
 } from "./message-queue.js";
 
 export interface ExecutionEngineOptions {
+  name?: string;
+  description?: string;
   model?: ChatModel;
   tools?: Agent[];
   agents?: Agent[];
@@ -26,10 +28,12 @@ export class ExecutionEngine extends EventEmitter {
     path,
     ...options
   }: { path: string } & ExecutionEngineOptions): Promise<ExecutionEngine> {
-    const { model, agents, tools } = await load({ path });
+    const { model, agents, tools, ...aigne } = await load({ path });
     return new ExecutionEngine({
       model,
       ...options,
+      name: options.name || aigne.name || undefined,
+      description: options.description || aigne.description || undefined,
       agents: agents.concat(options.agents ?? []),
       tools: tools.concat(options.tools ?? []),
     });
@@ -39,6 +43,8 @@ export class ExecutionEngine extends EventEmitter {
     if (options) checkArguments("ExecutionEngine", executionEngineOptionsSchema, options);
 
     super();
+    this.name = options?.name;
+    this.description = options?.description;
     this.model = options?.model;
     this.limits = options?.limits;
     if (options?.tools?.length) this.tools.push(...options.tools);
@@ -47,6 +53,10 @@ export class ExecutionEngine extends EventEmitter {
     this.initProcessExitHandler();
   }
 
+  name?: string;
+
+  description?: string;
+
   readonly messageQueue = new MessageQueue();
 
   model?: ChatModel;
diff --git a/packages/core/src/loader/index.ts b/packages/core/src/loader/index.ts
index a4d3c44b0..a7d1ff197 100644
--- a/packages/core/src/loader/index.ts
+++ b/packages/core/src/loader/index.ts
@@ -37,6 +37,7 @@ export async function load(options: LoadOptions) {
   );
 
   return {
+    ...aigne,
     model: await loadModel(aigne.chat_model),
     agents,
     tools,
@@ -111,6 +112,8 @@ async function loadModel(
 }
 
 const aigneFileSchema = z.object({
+  name: z.string().nullish(),
+  description: z.string().nullish(),
   chat_model: z
     .union([
       z.string(),
diff --git a/packages/core/test-agents/aigne.yaml b/packages/core/test-agents/aigne.yaml
index fd8cd8587..8b6f01d02 100644
--- a/packages/core/test-agents/aigne.yaml
+++ b/packages/core/test-agents/aigne.yaml
@@ -1,3 +1,5 @@
+name: test_aigne_project
+description: A test project for the aigne agent
 chat_model:
   name: gpt-4o-mini
   temperature: 0.8
diff --git a/packages/core/test/_mocks/mock-mcp-server-sse.ts b/packages/core/test/_mocks/mock-mcp-server-sse.ts
index 615b4c75f..f518535de 100644
--- a/packages/core/test/_mocks/mock-mcp-server-sse.ts
+++ b/packages/core/test/_mocks/mock-mcp-server-sse.ts
@@ -56,9 +56,7 @@ export function mockMCPSSEServer(port: number) {
   app.get("/sse", async (_: Request, res: Response) => {
     const transport = new SSEServerTransport("/messages", res);
     transports[transport.sessionId] = transport;
-    console.log("New transport connected", transport.sessionId);
     await server.connect(transport);
-    console.log("New transport connected end", transport.sessionId);
     res.flushHeaders();
   });
 
diff --git a/packages/core/test/loader/loader.test.ts b/packages/core/test/loader/loader.test.ts
index bfae53d48..3cffe5671 100644
--- a/packages/core/test/loader/loader.test.ts
+++ b/packages/core/test/loader/loader.test.ts
@@ -15,6 +15,13 @@ test("ExecutionEngine.load should load agents correctly", async () => {
     path: join(import.meta.dirname, "../../test-agents"),
   });
 
+  expect(engine).toEqual(
+    expect.objectContaining({
+      name: "test_aigne_project",
+      description: "A test project for the aigne agent",
+    }),
+  );
+
   expect(engine.agents.length).toBe(1);
 
   const chat = engine.agents[0];
@@ -105,22 +112,26 @@ test("load should process path correctly", async () => {
   stat.mockReturnValueOnce(Promise.resolve({ isDirectory: () => true }));
   exists.mockReturnValueOnce(Promise.resolve(true));
   readFile.mockReturnValueOnce("chat_model: gpt-4o-mini");
-  expect(load({ path: "foo" })).resolves.toEqual({
-    model: expect.anything(),
-    agents: [],
-    tools: [],
-  });
+  expect(load({ path: "foo" })).resolves.toEqual(
+    expect.objectContaining({
+      model: expect.anything(),
+      agents: [],
+      tools: [],
+    }),
+  );
   expect(readFile).toHaveBeenLastCalledWith("foo/aigne.yaml", "utf8");
 
   // mock a directory with a .yml file
   stat.mockReturnValueOnce(Promise.resolve({ isDirectory: () => true }));
   exists.mockReturnValueOnce(Promise.resolve(false)).mockReturnValueOnce(Promise.resolve(true));
   readFile.mockReturnValueOnce("chat_model: gpt-4o-mini");
-  expect(load({ path: "bar" })).resolves.toEqual({
-    model: expect.anything(),
-    agents: [],
-    tools: [],
-  });
+  expect(load({ path: "bar" })).resolves.toEqual(
+    expect.objectContaining({
+      model: expect.anything(),
+      agents: [],
+      tools: [],
+    }),
+  );
   expect(readFile).toHaveBeenLastCalledWith("bar/aigne.yml", "utf8");
 });
 
diff --git a/packages/core/test/tsconfig.json b/packages/core/test/tsconfig.json
new file mode 100644
index 000000000..739ded1d9
--- /dev/null
+++ b/packages/core/test/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "paths": {
+      "@aigne/core": ["../src"],
+      "@aigne/core/*": ["../src/*"]
+    }
+  }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8428ec9eb..9ad8befa0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -192,12 +192,18 @@ importers:
       '@aigne/core':
         specifier: workspace:^
         version: link:../core
+      '@modelcontextprotocol/sdk':
+        specifier: ^1.8.0
+        version: 1.8.0
       chalk:
         specifier: ^5.4.1
         version: 5.4.1
       commander:
         specifier: ^13.1.0
         version: 13.1.0
+      express:
+        specifier: ^5.1.0
+        version: 5.1.0
       gradient-string:
         specifier: ^3.0.0
         version: 3.0.0
@@ -207,6 +213,9 @@ importers:
       openai:
         specifier: ^4.91.1
         version: 4.91.1(encoding@0.1.13)(ws@8.18.0)(zod@3.24.2)
+      zod:
+        specifier: ^3.24.2
+        version: 3.24.2
     devDependencies:
       '@aigne/test-utils':
         specifier: workspace:^
@@ -214,12 +223,18 @@ importers:
       '@types/bun':
         specifier: ^1.2.8
         version: 1.2.8
+      '@types/express':
+        specifier: ^5.0.1
+        version: 5.0.1
       '@types/gradient-string':
         specifier: ^1.1.6
         version: 1.1.6
       '@types/node':
         specifier: ^22.14.0
         version: 22.14.0
+      detect-port:
+        specifier: ^2.1.0
+        version: 2.1.0
       npm-run-all:
         specifier: ^4.1.5
         version: 4.1.5
@@ -229,6 +244,9 @@ importers:
       typescript:
         specifier: ^5.8.2
         version: 5.8.2
+      ufo:
+        specifier: ^1.6.0
+        version: 1.6.0
 
   packages/core:
     dependencies:
@@ -1621,6 +1639,9 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  ufo@1.6.0:
+    resolution: {integrity: sha512-AkgU2cV/+Xb4Uz6cic0kMZbtM42nbltnGvTVOt/8gMCbO2/z64nE47TOygh7HjgFPkUkVRBEyNFqpqi3zo+BJA==}
+
   unbox-primitive@1.1.0:
     resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
     engines: {node: '>= 0.4'}
@@ -3187,6 +3208,8 @@ snapshots:
 
   typescript@5.8.2: {}
 
+  ufo@1.6.0: {}
+
   unbox-primitive@1.1.0:
     dependencies:
       call-bound: 1.0.3


Reviews:
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

<details>
<summary>Commits</summary>
Files that changed from the base of the PR and between 1f0d34ddda3154283a4bc958ddb9b68b4ac106b0 and b5317f4ad2351395dc2493300e4e128b5ebb8955 commits.
</details>

<details>
<summary>Files selected (16)</summary>

* packages/cli/src/commands/aigne.ts (2)
* packages/cli/src/commands/serve-mcp.ts (1)
* packages/cli/src/utils/serve-mcp.ts (1)
* packages/cli/test/cli.test.ts (1)
* packages/cli/test/commands/aigne.test.ts (1)
* packages/cli/test/commands/run.test.ts (1)
* packages/cli/test/commands/serve-mcp.test.ts (1)
* packages/cli/test/commands/test.test.ts (1)
* packages/cli/test/utils/serve-mcp.test.ts (1)
* packages/cli/tsconfig.build.json (1)
* packages/cli/tsconfig.json (1)
* packages/core/src/agents/agent.ts (1)
* packages/core/src/execution-engine/execution-engine.ts (4)
* packages/core/src/loader/index.ts (2)
* packages/core/test/_mocks/mock-mcp-server-sse.ts (1)
* packages/core/test/loader/loader.test.ts (1)
</details>


<details>
<summary>Files ignored due to filter (4)</summary>

* packages/cli/package.json
* packages/cli/test-agents/aigne.yaml
* packages/core/test-agents/aigne.yaml
* pnpm-lock.yaml

</details>






<details>
<summary>Files skipped from review due to trivial changes (5)</summary>

* packages/cli/test/commands/run.test.ts
* packages/cli/test/commands/test.test.ts
* packages/cli/tsconfig.build.json
* packages/cli/tsconfig.json
* packages/core/test/_mocks/mock-mcp-server-sse.ts

</details>

<details>
<summary>Review comments generated (38)</summary>

* Review: 16
* LGTM: 22

</details>

---

<details>
<summary>提示</summary>

### 与 <img src="https://avatars.githubusercontent.com/in/347564?s=41&u=fad245b8b4c7254fe63dd4dcd4d662ace122757e&v=4" alt="Image description" width="20" height="20"> AIGNE Reviewer (`@aignebot`) 聊天
- 回复此机器人留下的审查评论以提出后续问题。审查评论是对差异或文件的评论。
- 通过在回复中标记 `@aignebot` 邀请机器人加入审查评论链。

### 代码建议
- 机器人可能会提出代码建议，但在提交前请仔细审查它们，因为行号范围可能会不对齐。
- 你可以编辑机器人做出的评论，并在建议稍有偏差时手动调整。

### 暂停增量审查
- 在 PR 描述中的任何位置添加 `@aignebot: ignore` 以暂停机器人的进一步审查。

</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T08:16:25Z
user: login: wangshijun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:20:06Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T09:02:17Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T11:09:30Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T12:19:36Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T12:59:22Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T13:03:34Z
user: login: wangshijun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T22:10:17Z
user: login: li-yechao
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T00:31:13Z
user: login: wangshijun
body: let's ship it
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T00:43:20Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+}
+
+export function createServeMCPCommand(): Command {
+  return new Command("serve-mcp")
+    .description("Serve the agents in the specified directory as a MCP server")
+    .argument("[path]", "Path to the agents directory", ".")
+    .option("--port <port>", "Port to run the MCP server on", "3000")
path: packages/cli/src/commands/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The port option should be parsed as a number and validated to be within valid port range (1-65535):
```diff
-    .option("--port <port>", "Port to run the MCP server on", "3000")
+    .option("--port <port>", "Port to run the MCP server on", parseInt, "3000")
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+}
+
+export function createServeMCPCommand(): Command {
+  return new Command("serve-mcp")
+    .description("Serve the agents in the specified directory as a MCP server")
+    .argument("[path]", "Path to the agents directory", ".")
+    .option("--port <port>", "Port to run the MCP server on", "3000")
+    .action(async (path: string, options: ServeMCPOptions) => {
+      const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
+
+      const engine = await ExecutionEngine.load({ path: absolutePath });
path: packages/cli/src/commands/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

Error handling is missing for the engine load operation which could fail:
```diff
-      const engine = await ExecutionEngine.load({ path: absolutePath });
+      try {
+        const engine = await ExecutionEngine.load({ path: absolutePath });
+      } catch (err) {
+        console.error(`Failed to load execution engine from ${absolutePath}:`, err);
+        process.exit(1);
+      }
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+}
+
+export function createServeMCPCommand(): Command {
+  return new Command("serve-mcp")
+    .description("Serve the agents in the specified directory as a MCP server")
+    .argument("[path]", "Path to the agents directory", ".")
+    .option("--port <port>", "Port to run the MCP server on", "3000")
+    .action(async (path: string, options: ServeMCPOptions) => {
+      const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
+
+      const engine = await ExecutionEngine.load({ path: absolutePath });
+
+      serveMCPServer({ engine, port: options.port });
path: packages/cli/src/commands/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The serveMCPServer call lacks error handling and appears to be synchronous despite likely being an async operation:
```diff
-      serveMCPServer({ engine, port: options.port });
+      try {
+        await serveMCPServer({ engine, port: Number(options.port) });
+      } catch (err) {
+        console.error('Failed to start MCP server:', err);
+        process.exit(1);
+      }
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -87,18 +87,18 @@ export abstract class Agent<I extends Message = Message, O extends Message = Mes
 
   private readonly _outputSchema?: AgentInputOutputSchema<O>;
 
-  get inputSchema(): ZodType<I> {
+  get inputSchema(): ZodObject<{ [key in keyof I]: ZodType<I[key]> }> {
     const s = this._inputSchema;
     const schema = typeof s === "function" ? s(this) : s || z.object({});
     checkAgentInputOutputSchema(schema);
-    return schema.passthrough() as unknown as ZodType<I>;
+    return schema.passthrough() as unknown as ZodObject<{ [key in keyof I]: ZodType<I[key]> }>;
path: packages/core/src/agents/agent.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The type casting to `unknown` before the final type is unnecessary and could mask type errors. A direct cast would be safer:
```diff
-    return schema.passthrough() as unknown as ZodObject<{ [key in keyof I]: ZodType<I[key]> }>;
+    return schema.passthrough() as ZodObject<{ [key in keyof I]: ZodType<I[key]> }>;
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -87,18 +87,18 @@
 
   private readonly _outputSchema?: AgentInputOutputSchema<O>;
 
-  get inputSchema(): ZodType<I> {
+  get inputSchema(): ZodObject<{ [key in keyof I]: ZodType<I[key]> }> {
     const s = this._inputSchema;
     const schema = typeof s === "function" ? s(this) : s || z.object({});
     checkAgentInputOutputSchema(schema);
-    return schema.passthrough() as unknown as ZodType<I>;
+    return schema.passthrough() as unknown as ZodObject<{ [key in keyof I]: ZodType<I[key]> }>;
   }
 
-  get outputSchema(): ZodType<O> {
+  get outputSchema(): ZodObject<{ [key in keyof O]: ZodType<O[key]> }> {
     const s = this._outputSchema;
     const schema = typeof s === "function" ? s(this) : s || z.object({});
     checkAgentInputOutputSchema(schema);
-    return schema.passthrough() as unknown as ZodType<O>;
+    return schema.passthrough() as unknown as ZodObject<{ [key in keyof O]: ZodType<O[key]> }>;
path: packages/core/src/agents/agent.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The type casting to `unknown` before the final type is unnecessary and could mask type errors. A direct cast would be safer:
```diff
-    return schema.passthrough() as unknown as ZodObject<{ [key in keyof O]: ZodType<O[key]> }>;
+    return schema.passthrough() as ZodObject<{ [key in keyof O]: ZodType<O[key]> }>;
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import { expect, spyOn, test } from "bun:test";
+import assert from "node:assert";
+import { join } from "node:path";
+import { serveMCPServer } from "@aigne/cli/utils/serve-mcp.js";
+import { ExecutionEngine } from "@aigne/core";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { detect } from "detect-port";
+
+test("serveMCPServer should work", async () => {
+  const port = await detect();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+  const engine = await ExecutionEngine.load({ path: testAgentsPath });
+
+  assert(engine.model, "engine.model should be defined");
+  spyOn(engine.model, "call").mockReturnValueOnce(
+    Promise.resolve({
+      text: "hello, how can I help you?",
+    }),
+  );
+
+  const server = await serveMCPServer({ engine, port });
+
+  const url = `http://localhost:${port}/sse`;
+  const transport = new SSEClientTransport(new URL(url));
+  const client = new Client({
+    name: "test-client",
+    version: "1.0.0",
+  });
+
+  await client.connect(transport);
+  expect(client.getServerVersion()).toEqual({ name: "test_aigne_project", version: "1.0.0" });
+
+  expect(client.listTools()).resolves.toEqual({
+    tools: [
+      expect.objectContaining({
+        name: "chat",
+      }),
+    ],
+  });
+
+  expect(client.callTool({ name: "chat", arguments: { $message: "hello" } })).resolves.toEqual({
+    content: [{ type: "text", text: "hello, how can I help you?" }],
+  });
+
+  await client.close();
+  await new Promise((resolve) => setTimeout(resolve, 3000));
+  server.closeAllConnections();
path: packages/cli/test/utils/serve-mcp.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The server cleanup has potential issues with connection handling and timing:
```diff
-  await client.close();
-  await new Promise((resolve) => setTimeout(resolve, 3000));
-  server.closeAllConnections();
+  await client.close();
+  server.closeAllConnections();
+  await new Promise((resolve) => {
+    server.on('close', resolve);
+    setTimeout(resolve, 5000); // Fallback timeout
+  });
```

The arbitrary 3s timeout should be replaced with an event-based approach to ensure proper cleanup.



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import { expect, spyOn, test } from "bun:test";
+import assert from "node:assert";
+import { join } from "node:path";
+import { serveMCPServer } from "@aigne/cli/utils/serve-mcp.js";
+import { ExecutionEngine } from "@aigne/core";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { detect } from "detect-port";
+
+test("serveMCPServer should work", async () => {
+  const port = await detect();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+  const engine = await ExecutionEngine.load({ path: testAgentsPath });
+
+  assert(engine.model, "engine.model should be defined");
+  spyOn(engine.model, "call").mockReturnValueOnce(
+    Promise.resolve({
+      text: "hello, how can I help you?",
+    }),
+  );
+
+  const server = await serveMCPServer({ engine, port });
path: packages/cli/test/utils/serve-mcp.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The server initialization lacks error handling:
```diff
-  const server = await serveMCPServer({ engine, port });
+  const server = await serveMCPServer({ engine, port }).catch(err => {
+    throw new Error(`Failed to start MCP server: ${err.message}`);
+  });
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import { expect, spyOn, test } from "bun:test";
+import assert from "node:assert";
+import { join } from "node:path";
+import { serveMCPServer } from "@aigne/cli/utils/serve-mcp.js";
+import { ExecutionEngine } from "@aigne/core";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { detect } from "detect-port";
+
+test("serveMCPServer should work", async () => {
+  const port = await detect();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+  const engine = await ExecutionEngine.load({ path: testAgentsPath });
+
+  assert(engine.model, "engine.model should be defined");
+  spyOn(engine.model, "call").mockReturnValueOnce(
+    Promise.resolve({
+      text: "hello, how can I help you?",
+    }),
+  );
path: packages/cli/test/utils/serve-mcp.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The mock setup should include error cases:
```diff
   assert(engine.model, "engine.model should be defined");
-  spyOn(engine.model, "call").mockReturnValueOnce(
-    Promise.resolve({
-      text: "hello, how can I help you?",
-    }),
-  );
+  const mockCall = spyOn(engine.model, "call");
+  mockCall.mockReturnValueOnce(Promise.resolve({
+    text: "hello, how can I help you?",
+  }));
+  mockCall.mockReturnValueOnce(Promise.reject(new Error("Mock error")));
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:22Z
author_association: NONE
diff_hunk: @@ -26,10 +28,12 @@ export class ExecutionEngine extends EventEmitter {
     path,
     ...options
   }: { path: string } & ExecutionEngineOptions): Promise<ExecutionEngine> {
-    const { model, agents, tools } = await load({ path });
+    const { model, agents, tools, ...aigne } = await load({ path });
     return new ExecutionEngine({
       model,
       ...options,
+      name: options.name || aigne.name || undefined,
+      description: options.description || aigne.description || undefined,
path: packages/core/src/execution-engine/execution-engine.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The spread operator usage could lead to unintended property overrides. The current order may not properly handle all edge cases. Here's a more robust approach:

```diff
-    const { model, agents, tools, ...aigne } = await load({ path });
-     return new ExecutionEngine({
-       model,
-       ...options,
-      name: options.name || aigne.name || undefined,
-      description: options.description || aigne.description || undefined,
+    const loaded = await load({ path });
+    return new ExecutionEngine({
+      ...loaded,
+      ...options,
+      agents: loaded.agents.concat(options.agents ?? []),
+      tools: loaded.tools.concat(options.tools ?? []),
```

This ensures:
1. Base configuration from loaded file takes precedence
2. Explicit options override loaded values
3. Arrays are properly concatenated


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -37,6 +37,7 @@ export async function load(options: LoadOptions) {
   );
 
   return {
+    ...aigne,
path: packages/core/src/loader/index.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The spread operator usage here could lead to property collisions if the model, agents, or tools properties exist in the aigne object. Should explicitly select needed properties or handle potential overwrites.
```diff
-    ...aigne,
+    name: aigne.name,
+    description: aigne.description,
+    chat_model: aigne.chat_model,
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,65 @@
+import type { Server } from "node:http";
+import { type ExecutionEngine, getMessage } from "@aigne/core";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import express, { type Request, type Response } from "express";
+
+export function serveMCPServer({ engine, port }: { engine: ExecutionEngine; port: number }) {
+  const server = new McpServer(
+    {
+      name: engine.name || "aigne-mcp-server",
+      version: "1.0.0",
+    },
+    {
+      capabilities: { tools: {} },
+      instructions: engine.description,
+    },
+  );
+
+  for (const agent of engine.agents) {
+    const schema = agent.inputSchema;
+
+    server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
+      const result = await engine.call(agent, input);
path: packages/cli/src/utils/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The agent iteration lacks error handling for invalid schemas or failed calls.
```diff
-  for (const agent of engine.agents) {
-    const schema = agent.inputSchema;
-
-    server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
-      const result = await engine.call(agent, input);

+  for (const agent of engine.agents) {
+    try {
+      const schema = agent.inputSchema;
+      if (!schema?.shape) {
+        throw new Error(`Invalid schema for agent: ${agent.name}`);
+      }
+
+      server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
+        try {
+          const result = await engine.call(agent, input);
+          if (!result) {
+            throw new Error('Engine call returned no result');
+          }
+          return {
+            content: [{
+              type: "text", 
+              text: getMessage(result) || JSON.stringify(result),
+            }],
+          };
+        } catch (err) {
+          console.error(`Error executing agent ${agent.name}:`, err);
+          throw err;
+        }
+      });
+    } catch (err) {
+      console.error(`Error registering agent ${agent.name}:`, err);
+    }
+  }
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,65 @@
+import type { Server } from "node:http";
+import { type ExecutionEngine, getMessage } from "@aigne/core";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import express, { type Request, type Response } from "express";
+
+export function serveMCPServer({ engine, port }: { engine: ExecutionEngine; port: number }) {
+  const server = new McpServer(
+    {
+      name: engine.name || "aigne-mcp-server",
+      version: "1.0.0",
+    },
+    {
+      capabilities: { tools: {} },
+      instructions: engine.description,
+    },
+  );
+
+  for (const agent of engine.agents) {
+    const schema = agent.inputSchema;
+
+    server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
+      const result = await engine.call(agent, input);
+
+      return {
+        content: [
+          {
+            type: "text",
+            text: getMessage(result) || JSON.stringify(result),
+          },
+        ],
+      };
+    });
+  }
+
+  const app = express();
+
+  const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+  app.get("/sse", async (_: Request, res: Response) => {
+    const transport = new SSEServerTransport("/messages", res);
+    transports[transport.sessionId] = transport;
+    res.on("close", () => {
+      delete transports[transport.sessionId];
+    });
+    await server.connect(transport);
+  });
path: packages/cli/src/utils/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The SSE endpoint lacks error handling and proper cleanup.
```diff
-  app.get("/sse", async (_: Request, res: Response) => {
-    const transport = new SSEServerTransport("/messages", res);
-    transports[transport.sessionId] = transport;
-    res.on("close", () => {
-      delete transports[transport.sessionId];
-    });
-    await server.connect(transport);
-  });

+  app.get("/sse", async (_: Request, res: Response) => {
+    try {
+      const transport = new SSEServerTransport("/messages", res);
+      transports[transport.sessionId] = transport;
+      
+      const cleanup = () => {
+        delete transports[transport.sessionId];
+        transport.close?.();
+      };
+      
+      res.on("close", cleanup);
+      res.on("error", cleanup);
+      
+      await server.connect(transport);
+    } catch (err) {
+      console.error('Error establishing SSE connection:', err);
+      res.status(500).end();
+    }
+  });
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,65 @@
+import type { Server } from "node:http";
+import { type ExecutionEngine, getMessage } from "@aigne/core";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import express, { type Request, type Response } from "express";
+
+export function serveMCPServer({ engine, port }: { engine: ExecutionEngine; port: number }) {
+  const server = new McpServer(
+    {
+      name: engine.name || "aigne-mcp-server",
+      version: "1.0.0",
+    },
+    {
+      capabilities: { tools: {} },
+      instructions: engine.description,
+    },
+  );
+
+  for (const agent of engine.agents) {
+    const schema = agent.inputSchema;
+
+    server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
+      const result = await engine.call(agent, input);
+
+      return {
+        content: [
+          {
+            type: "text",
+            text: getMessage(result) || JSON.stringify(result),
+          },
+        ],
+      };
+    });
+  }
+
+  const app = express();
+
+  const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+  app.get("/sse", async (_: Request, res: Response) => {
+    const transport = new SSEServerTransport("/messages", res);
+    transports[transport.sessionId] = transport;
+    res.on("close", () => {
+      delete transports[transport.sessionId];
+    });
+    await server.connect(transport);
+  });
+
+  app.post("/messages", async (req: Request, res: Response) => {
+    const sessionId = req.query.sessionId as string;
+    const transport = transports[sessionId];
+    if (transport) {
+      await transport.handlePostMessage(req, res);
+    } else {
+      res.status(400).send("No transport found for sessionId");
+    }
+  });
path: packages/cli/src/utils/serve-mcp.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The messages endpoint needs input validation and proper error handling.
```diff
-  app.post("/messages", async (req: Request, res: Response) => {
-    const sessionId = req.query.sessionId as string;
-    const transport = transports[sessionId];
-    if (transport) {
-      await transport.handlePostMessage(req, res);
-    } else {
-      res.status(400).send("No transport found for sessionId");
-    }
-  });

+  app.post("/messages", async (req: Request, res: Response) => {
+    try {
+      const sessionId = req.query.sessionId;
+      if (!sessionId || typeof sessionId !== 'string') {
+        return res.status(400).send('Invalid or missing sessionId');
+      }
+      
+      const transport = transports[sessionId];
+      if (!transport) {
+        return res.status(404).send("No transport found for sessionId");
+      }
+      
+      await transport.handlePostMessage(req, res);
+    } catch (err) {
+      console.error('Error handling message:', err);
+      res.status(500).send('Internal server error');
+    }
+  });
```



<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -4,11 +4,11 @@ import { Command } from "commander";
 test("CLI imports and loads correctly", async () => {
   const createAIGNECommand = mock(() => new Command());
 
-  mock.module("../src/commands/aigne.js", () => ({
+  mock.module("@aigne/cli/commands/aigne.js", () => ({
     createAIGNECommand,
   }));
 
-  await import("../src/cli.js");
+  await import("@aigne/cli/cli.js");
path: packages/cli/test/cli.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The import paths have been changed to use package imports instead of relative paths. While this works, it could cause issues with test isolation and mocking. Consider using relative paths in tests to:
1. Avoid potential module resolution conflicts
2. Make tests more resilient to package name changes
3. Ensure tests run against the local source code

```diff
-  mock.module("@aigne/cli/commands/aigne.js", () => ({
+  mock.module("../src/commands/aigne.js", () => ({
     createAIGNECommand,
   }));
 
-  await import("@aigne/cli/cli.js");
+  await import("../src/cli.js");
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,49 @@
+import { expect, mock, test } from "bun:test";
+import { join } from "node:path";
+import { createServeMCPCommand } from "@aigne/cli/commands/serve-mcp.js";
+import { mockModule } from "@aigne/test-utils/mock-module.js";
+import { detect } from "detect-port";
+
+test("serve-mcp command should work with default options", async () => {
+  const mockListen = mock();
+  const mockExpress = mock(() => ({
+    get: mock(),
+    post: mock(),
+    listen: mockListen,
+  }));
+
+  await using _ = await mockModule("express", () => ({
+    default: mockExpress,
+  }));
+
+  const command = createServeMCPCommand();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+
+  process.chdir(testAgentsPath);
+  await command.parseAsync(["", "serve-mcp"]);
+  expect(mockListen).toHaveBeenCalledWith("3000", expect.any(Function));
+});
path: packages/cli/test/commands/serve-mcp.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The test is missing error handling for the case where the port 3000 is already in use. Should use detect-port like in the second test.
```diff
- await command.parseAsync(["", "serve-mcp"]);
- expect(mockListen).toHaveBeenCalledWith("3000", expect.any(Function));
+ const defaultPort = await detect(3000);
+ await command.parseAsync(["", "serve-mcp"]);
+ expect(mockListen).toHaveBeenCalledWith(defaultPort.toString(), expect.any(Function));
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,49 @@
+import { expect, mock, test } from "bun:test";
+import { join } from "node:path";
+import { createServeMCPCommand } from "@aigne/cli/commands/serve-mcp.js";
+import { mockModule } from "@aigne/test-utils/mock-module.js";
+import { detect } from "detect-port";
+
+test("serve-mcp command should work with default options", async () => {
+  const mockListen = mock();
+  const mockExpress = mock(() => ({
+    get: mock(),
+    post: mock(),
+    listen: mockListen,
+  }));
+
+  await using _ = await mockModule("express", () => ({
+    default: mockExpress,
+  }));
+
+  const command = createServeMCPCommand();
+
+  const testAgentsPath = join(import.meta.dirname, "../../test-agents");
+
+  process.chdir(testAgentsPath);
+  await command.parseAsync(["", "serve-mcp"]);
path: packages/cli/test/commands/serve-mcp.test.ts
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

The test is modifying the process working directory which could affect other tests running in parallel. Should restore it after the test.
```diff
+ const originalCwd = process.cwd();
  process.chdir(testAgentsPath);
  await command.parseAsync(["", "serve-mcp"]);
+ process.chdir(originalCwd);
```


<!-- This is an auto-generated comment by AIGNE Reviewer -->
created_at: 2025-04-07T08:16:24Z
author_association: NONE
diff_hunk: @@ -3,6 +3,7 @@ import pkg from "../../package.json" with { type: "json" };
 import { asciiLogo } from "../utils/ascii-logo.js";
 import { createCreateCommand } from "./create.js";
 import { createRunCommand } from "./run.js";
+import { createServeMCPCommand } from "./serve-mcp.js";
path: packages/cli/src/commands/aigne.ts
user: login: wangshijun
body: 建议把这个 serve 改为可以扩展的，免的未来出现 breaking change，我想到的交互模式

`aigne serve --mcp` 启用1个 mcp
`aigne serve --web` 启用 web，在 aigne-studio 的 chat 组件抽出来之后

甚至两者融合
created_at: 2025-04-07T08:20:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,6 +3,7 @@ import pkg from "../../package.json" with { type: "json" };
 import { asciiLogo } from "../utils/ascii-logo.js";
 import { createCreateCommand } from "./create.js";
 import { createRunCommand } from "./run.js";
+import { createServeMCPCommand } from "./serve-mcp.js";
path: packages/cli/src/commands/aigne.ts
user: login: li-yechao
body: 我一开始也想用一个命令统一 serve，但又考虑到这些 serve 的方式会有不同的参数，容易导致混乱😂。
created_at: 2025-04-07T09:02:15Z
author_association: COLLABORATOR
diff_hunk: @@ -26,10 +28,12 @@ export class ExecutionEngine extends EventEmitter {
     path,
     ...options
   }: { path: string } & ExecutionEngineOptions): Promise<ExecutionEngine> {
-    const { model, agents, tools } = await load({ path });
+    const { model, agents, tools, ...aigne } = await load({ path });
     return new ExecutionEngine({
       model,
       ...options,
+      name: options.name || aigne.name || undefined,
+      description: options.description || aigne.description || undefined,
path: packages/core/src/execution-engine/execution-engine.ts
user: login: li-yechao
body: 没有问题
created_at: 2025-04-07T11:09:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,65 @@
+import type { Server } from "node:http";
+import { type ExecutionEngine, getMessage } from "@aigne/core";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import express, { type Request, type Response } from "express";
+
+export function serveMCPServer({ engine, port }: { engine: ExecutionEngine; port: number }) {
+  const server = new McpServer(
+    {
+      name: engine.name || "aigne-mcp-server",
+      version: "1.0.0",
+    },
+    {
+      capabilities: { tools: {} },
+      instructions: engine.description,
+    },
+  );
+
+  for (const agent of engine.agents) {
+    const schema = agent.inputSchema;
+
+    server.tool(agent.name, agent.description || "", schema.shape, async (input) => {
+      const result = await engine.call(agent, input);
+
+      return {
+        content: [
+          {
+            type: "text",
+            text: getMessage(result) || JSON.stringify(result),
+          },
+        ],
+      };
+    });
+  }
+
+  const app = express();
+
+  const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+  app.get("/sse", async (_: Request, res: Response) => {
+    const transport = new SSEServerTransport("/messages", res);
+    transports[transport.sessionId] = transport;
+    res.on("close", () => {
+      delete transports[transport.sessionId];
+    });
+    await server.connect(transport);
+  });
path: packages/cli/src/utils/serve-mcp.ts
user: login: li-yechao
body: 已通过 express 全局处理
created_at: 2025-04-07T12:19:34Z
author_association: COLLABORATOR
diff_hunk: @@ -3,6 +3,7 @@ import pkg from "../../package.json" with { type: "json" };
 import { asciiLogo } from "../utils/ascii-logo.js";
 import { createCreateCommand } from "./create.js";
 import { createRunCommand } from "./run.js";
+import { createServeMCPCommand } from "./serve-mcp.js";
path: packages/cli/src/commands/aigne.ts
user: login: li-yechao
body: fixed
created_at: 2025-04-07T12:59:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,24 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+}
+
+export function createServeMCPCommand(): Command {
+  return new Command("serve-mcp")
+    .description("Serve the agents in the specified directory as a MCP server")
+    .argument("[path]", "Path to the agents directory", ".")
+    .option("--port <port>", "Port to run the MCP server on", "3000")
path: packages/cli/src/commands/serve-mcp.ts
user: login: li-yechao
body: fixed
created_at: 2025-04-07T13:03:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,27 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+  mcp?: boolean;
+}
+
+export function createServeCommand(): Command {
path: packages/cli/src/commands/serve.ts
user: login: wangshijun
body: 这里的 port 需要遵循标准的 process.env.PORT
created_at: 2025-04-07T22:08:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import { isAbsolute, resolve } from "node:path";
+import { ExecutionEngine } from "@aigne/core";
+import { Command, type OptionValues } from "commander";
+import { serveMCPServer } from "../utils/serve-mcp.js";
+
+interface ServeMCPOptions extends OptionValues {
+  port: number;
+  mcp?: boolean;
+}
+
+export function createServeCommand(): Command {
path: packages/cli/src/commands/serve.ts
user: login: li-yechao
body: fixed
created_at: 2025-04-08T00:31:11Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T08:14:44Z
body: <img src="https://avatars.githubusercontent.com/u/143965177?s=48&v=4" alt="Image description" width="15" height="15"> AIGNE Framework

## Walkthrough
This update enhances the execution engine with metadata support (name and description), improves type safety by replacing generic ZodType with specific ZodObject types, and streamlines test code. The changes focus on better configuration management, stronger type guarantees, and cleaner test implementations while maintaining core functionality.

## Changes
| Files | Summary |
|-------|----------|
| `packages/core/src/execution-engine/execution-engine.ts`<br>`packages/core/src/loader/index.ts`<br>`packages/core/test/loader/loader.test.ts` | Implements metadata support in ExecutionEngine and configuration schema, with corresponding test coverage |
| `packages/core/src/agents/agent.ts`<br>`packages/cli/tsconfig.json` | Enhances type safety with specific ZodObject types and updates CLI path mappings |
| `packages/core/test/_mocks/mock-mcp-server-sse.ts` | Removes debug logging from mock MCP SSE server implementation |
<!-- This is an auto-generated comment: raw summary by AIGNE Reviewer -->
<!--

I'll group these changesets into logically related changes:

1. ExecutionEngine and Configuration Enhancement:
```
packages/core/src/execution-engine/execution-engine.ts
packages/core/src/loader/index.ts
packages/core/test/loader/loader.test.ts
```
Summary: Adds metadata support (name and description) to ExecutionEngine and extends the configuration schema to support these fields. Updates tests to verify the new metadata functionality.

2. Type System Improvements:
```
packages/core/src/agents/agent.ts
packages/cli/tsconfig.json
```
Summary: Strengthens type safety by replacing generic ZodType with specific ZodObject types in Agent class and adds CLI-specific path mappings in TypeScript configuration.

3. Test Cleanup:
```
packages/core/test/_mocks/mock-mcp-server-sse.ts
```
Summary: Removes debug logging statements from mock MCP SSE server implementation while preserving core functionality.

The changes have been consolidated from 6 individual files into 3 logical groups based on their related functionality. The first group focuses on adding metadata support across the execution engine and configuration system, the second group improves type safety and module resolution, and the third group cleans up test code.

This grouping better reflects the cohesive changes being made to enhance the execution engine's metadata capabilities, improve type safety, and maintain test code quality.
-->
<!-- end of auto-generated comment: raw summary by AIGNE Reviewer -->
<!-- This is an auto-generated comment: short summary by AIGNE Reviewer -->
<!--

Summary of Changes:

The changes introduce metadata support (name and description fields) to the ExecutionEngine component and its configuration schema. This is implemented across the execution engine and loader modules, with corresponding test updates to validate the new metadata functionality.

Type system improvements include replacing generic ZodType with more specific ZodObject types in the Agent class, along with updated TypeScript path mappings in the CLI configuration.

Additionally, debug logging statements have been removed from the mock MCP SSE server implementation in the test suite.

The changes span across:
- Core execution engine and configuration components
- Agent type definitions
- CLI TypeScript configuration
- Test infrastructure
-->
<!-- end of auto-generated comment: short summary by AIGNE Reviewer -->


<!-- commit_ids_reviewed_start -->
<!-- b5317f4ad2351395dc2493300e4e128b5ebb8955 -->
<!-- commit_ids_reviewed_end -->

<!-- This is an auto-generated comment: summarize by AIGNE Reviewer -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-07T13:00:30Z
body: ## [Codecov](https://app.codecov.io/gh/AIGNE-io/aigne-framework/pull/54?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AIGNE-io) Report
All modified and coverable lines are covered by tests :white_check_mark:

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE