Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"codebase-memory-mcp": {
"command": "codebase-memory-mcp",
"args": []
},
"puppeteer": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-puppeteer@2025.5.12"
]
}
}
}
325 changes: 269 additions & 56 deletions apps/api/README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/api/mcp/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { PluginRegistry } from "./registry.js";
import { ToolsDiscovery } from "./discovery.js";
import { ToolExecutor } from "./execute.js";
import { fileURLToPath } from "url";
import { fileManagerPlugin } from "./plugins/filemanager/manifest.js";
import { context7Plugin } from "./plugins/context7/manifest.js";
import { puppeteerPlugin } from "./plugins/puppeteer/manifest.js";

export const mcpRegistry = new PluginRegistry();

// Register plugins from modular layout manifests
mcpRegistry.registerPlugin(fileManagerPlugin);
mcpRegistry.registerPlugin(context7Plugin);
mcpRegistry.registerPlugin(puppeteerPlugin);

export const mcpDiscovery = new ToolsDiscovery(mcpRegistry);
export const mcpExecutor = new ToolExecutor(mcpDiscovery);
2 changes: 1 addition & 1 deletion apps/api/mcp/plugins/context7/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "../../../../src/utils/env.js";
import "../../../src/utils/env.js";
import { StdioMCPServer } from "../../stdio-server.js";

const context7Env: Record<string, string> = {};
Expand Down
9 changes: 9 additions & 0 deletions apps/api/mcp/plugins/puppeteer/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import "../../../src/utils/env.js";
import { StdioMCPServer } from "../../stdio-server.js";

export const puppeteerPlugin = new StdioMCPServer(
"puppeteer",
"npx",
["-y", "@modelcontextprotocol/server-puppeteer"],
{}
);
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.0",
"@modelcontextprotocol/server-puppeteer": "^2025.5.12",
"@repo/db": "*",
"@repo/shared": "*",
"cors": "^2.8.5",
Expand Down
87 changes: 86 additions & 1 deletion apps/api/src/agent/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import { runAgent } from "./loop.js";
import { createMemory } from "./memory.js";
import { llmClient } from "./llm.js";
import { llmClient, nextStep } from "./llm.js";

// Mock @repo/db
vi.mock("@repo/db", () => {
Expand Down Expand Up @@ -425,6 +425,91 @@ describe("Agent Module & Execution Loop", () => {
expect(result.reason).toBe("Approval not approved");
});

// 14) Parallel Tool Execution tests
describe("Parallel Tool Execution", () => {
it("should parse type: tool_calls from LLM and validate schemas", async () => {
vi.spyOn(llmClient, "callModel").mockResolvedValue(
JSON.stringify({
type: "tool_calls",
tool_calls: [
{ tool_name: "test_tool", arguments: { arg1: "val1" } },
{ tool_name: "test_tool", arguments: { arg1: "val2" } }
]
})
);

const memory = createMemory();
const tools = [
{
name: "test_tool",
description: "A test tool",
inputSchema: { type: "object", properties: { arg1: { type: "string" } } },
execute: vi.fn(),
}
];

const res = await nextStep(memory, tools);
expect(res.step.type).toBe("tool_calls");
if (res.step.type === "tool_calls") {
expect(res.step.tool_calls).toHaveLength(2);
expect(res.step.tool_calls[0]?.tool_name).toBe("test_tool");
}
});

it("should execute parallel tool calls successfully when allowed", async () => {
let callCount = 0;
vi.spyOn(llmClient, "callModel").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
return JSON.stringify({
type: "tool_calls",
tool_calls: [
{ tool_name: "test_tool", arguments: { arg1: "val1" } },
{ tool_name: "test_tool", arguments: { arg1: "val2" } }
]
});
}
return JSON.stringify({
type: "final_answer",
answer: "Finished parallel work.",
});
});

vi.mocked(decide).mockResolvedValue({
decision: "ALLOW",
});

vi.mocked(mcpExecutor.execute).mockResolvedValue("mockResult");

const result = await runAgent("Do parallel tasks", "conv-parallel-1");
expect(result.status).toBe("SUCCESS");
expect(result.answer).toBe("Finished parallel work.");
expect(mcpExecutor.execute).toHaveBeenCalledTimes(2);
expect(result.memory.toolResults).toContain("mockResult");
});

it("should request approval for parallel tool calls when pending", async () => {
vi.spyOn(llmClient, "callModel").mockResolvedValue(
JSON.stringify({
type: "tool_calls",
tool_calls: [
{ tool_name: "test_tool", arguments: { arg1: "val1" } }
]
})
);

vi.mocked(decide).mockResolvedValue({
decision: "PENDING",
reason: "approval-parallel-123",
});

const result = await runAgent("Do parallel task requiring approval", "conv-parallel-2");
expect(result.status).toBe("PENDING");
expect(result.approvalId).toBe("approval-parallel-123");
expect(result.memory.approvalId).toBe("approval-parallel-123");
});
});

describe("Gemini API Client Timeout", () => {
afterEach(() => {
vi.unstubAllGlobals();
Expand Down
Loading
Loading