diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 5399131..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,11 +0,0 @@ -You are an expert programmer whose task is to assist the user implement their requests within the current working directory. - -In order to perform file system operations, you MUST NOT USE the built-in tools you have (Read, Write, Glob, Edit): instead, you MUST USE the `filesystem` MCP server, which provides the following tools: - -- `read_file`: read a file, providing its path -- `write_file`: write a file, providing its path and content -- `edit_file`: edit a file, providing the old string and the new string to replace the old one with -- `list_files`: list all the available files -- `file_exists`: check whether or not a file exists, providing its path - -Using these tools, you should be able to provide the user with the assistance that they need. diff --git a/README.md b/README.md index 07da7f5..b1ea203 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Claude + AgentFS + LlamaIndex Workflows +# Coding Agent + AgentFS + LlamaIndex Workflows -A demo where we run Claude Code within a fully-virtualized file system ([AgentFS](https://github.com/tursodatabase/agentfs)), orchestrating it with [LlamaIndex Workflows](https://github.com/run-llama/workflows-ts) and adding the possibility of reading unstructured files (e.g. PDFs or Word/Google docs) with [LlamaCloud](https://cloud.llamaindex.ai). +A demo where we run Claude Code or Codex within a fully-virtualized file system ([AgentFS](https://github.com/tursodatabase/agentfs)), orchestrating it with [LlamaIndex Workflows](https://github.com/run-llama/workflows-ts) and adding the possibility of reading unstructured files (e.g. PDFs or Word/Google docs) with [LlamaCloud](https://cloud.llamaindex.ai). ## Set Up and Run @@ -18,13 +18,27 @@ pnpm install # you can use other package managers, but pnpm is preferred ``` +If you wish to use the demo with Codex, you need to install the Codex SDK separately (given the size of the library - 140+ MB - its download it disabled by default): + +```bash +pnpm add @openai/codex-sdk +``` + +Moreover, if you wish to run the demo with Codex, you also need to start the MCP server (from a different terminal window, but within the same directory): + +```bash +pnpm run mcp-start +``` + +The MCP will be live on `http://localhost:3000/mcp`, and you will need to add the MCP configuration in [config.toml](./codex/config.toml) to the global Codex configuration in `$HOME/.codex/config.toml`. If you want Codex to use the filesystem MCP by default, you will also need to copy the [AGENTS.md](./codex/AGENTS.md) file, containing the instructions on how to use the server. + Now run the demo with: ```bash # for the first time pnpm run start -# for follow-ups +# If you want to add more files to the database pnpm run clean-start ``` diff --git a/codex/config.toml b/codex/config.toml index ba18e15..15c2b11 100644 --- a/codex/config.toml +++ b/codex/config.toml @@ -1,12 +1,3 @@ -model = "gpt-4.1" -model_provider = "openai-chat-completions" - -[model_providers.openai-chat-completions] -name = "OpenAI using Chat Completions" -base_url = "https://api.openai.com/v1" -env_key = "OPENAI_API_KEY" -wire_api = "responses" - [features] rmcp_client = true diff --git a/src/codex.ts b/src/codex.ts index 29b643a..25b8dc9 100644 --- a/src/codex.ts +++ b/src/codex.ts @@ -67,7 +67,7 @@ async function handleItemStart(event: ThreadEvent) { ), ), ); - if (typeof event.item.error != "undefined") { + if (event.item.error) { console.log( red( bold( @@ -76,7 +76,7 @@ async function handleItemStart(event: ThreadEvent) { ), ); } else { - if (typeof event.item.result != "undefined") { + if (event.item.result) { let finalResult = ""; for (const block of event.item.result.content) { if (block.type == "text") { @@ -136,7 +136,7 @@ async function handleItemUpdated(event: ThreadEvent) { ), ), ); - if (typeof event.item.error != "undefined") { + if (event.item.error) { console.log( red( bold( @@ -145,7 +145,7 @@ async function handleItemUpdated(event: ThreadEvent) { ), ); } else { - if (typeof event.item.result != "undefined") { + if (event.item.result) { let finalResult = ""; for (const block of event.item.result.content) { if (block.type == "text") { @@ -201,7 +201,7 @@ async function handleItemCompleted(event: ThreadEvent) { ), ), ); - if (typeof event.item.error != "undefined") { + if (event.item.error) { console.log( red( bold( @@ -210,7 +210,7 @@ async function handleItemCompleted(event: ThreadEvent) { ), ); } else { - if (typeof event.item.result != "undefined") { + if (event.item.result) { let finalResult = ""; for (const block of event.item.result.content) { if (block.type == "text") { diff --git a/src/index.ts b/src/index.ts index 1910e8e..e433923 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,10 @@ async function main() { workflow.handle([startEvent], async (_context, event) => { await renderLogo(); if (notFromScratch) { + fs.copyFileSync("fs.db", "fsMcp.db"); + if (fs.existsSync("fs.db-wal")) { + fs.copyFileSync("fs.db-wal", "fsMcp.db-wal"); + } return filesRegisteredEvent.with(); } const wd = event.data.workingDirectory; @@ -45,6 +49,10 @@ async function main() { "Could not register the files within the AgentFS file system: check writing permissions in the current directory", }); } else { + fs.copyFileSync("fs.db", "fsMcp.db"); + if (fs.existsSync("fs.db-wal")) { + fs.copyFileSync("fs.db-wal", "fsMcp.db-wal"); + } return filesRegisteredEvent.with(); } }); @@ -53,7 +61,9 @@ async function main() { workflow.handle([filesRegisteredEvent], async (_context, _event) => { console.log( bold( - green("All the files have been uploaded to the AgentFS filesystem, what would you like to do now?"), + green( + "All the files have been uploaded to the AgentFS filesystem, what would you like to do now?", + ), ), ); return requestPromptEvent.with(); @@ -75,7 +85,9 @@ async function main() { } else { try { await runCodex(event.data.prompt, { resumeSession: event.data.resume }); + return stopEvent.with({ success: true, error: null }); } catch (error) { + console.error(error); return stopEvent.with({ success: false, error: JSON.stringify(error) }); } } @@ -118,11 +130,15 @@ async function main() { plan: planMode, }), ); - const finalEvent = (await resumedContext.stream.until(stopEvent).toArray()).at(-1); + const finalEvent = ( + await resumedContext.stream.until(stopEvent).toArray() + ).at(-1); if (typeof finalEvent != "undefined") { if ("error" in finalEvent.data) { if (typeof finalEvent.data.error == "string") { - console.log(`${bold(red("An error occurred during the workflow execution:"))} ${finalEvent.data.error}`) + console.log( + `${bold(red("An error occurred during the workflow execution:"))} ${finalEvent.data.error}`, + ); } } } diff --git a/src/mcpServer.ts b/src/mcpServer.ts index d4970ba..5f955f1 100644 --- a/src/mcpServer.ts +++ b/src/mcpServer.ts @@ -1,8 +1,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Request, Response } from 'express'; +import { Request, Response } from "express"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { type CallToolResult, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import { randomUUID } from 'node:crypto'; +import { + type CallToolResult, + isInitializeRequest, +} from "@modelcontextprotocol/sdk/types.js"; +import { randomUUID } from "node:crypto"; import { readSchemaShape, fileExistsSchemaShape, @@ -33,7 +36,7 @@ const getServer = () => { inputSchema: readSchemaShape, }, async ({ filePath }) => { - const agentfs = await getAgentFS({}); + const agentfs = await getAgentFS({ filePath: "fsMcp.db" }); const content = await readFile(filePath, agentfs); if (typeof content == "string") { return { content: [{ type: "text", text: content }] }; @@ -58,7 +61,7 @@ const getServer = () => { inputSchema: fileExistsSchemaShape, }, async ({ filePath }) => { - const agentfs = await getAgentFS({}); + const agentfs = await getAgentFS({ filePath: "fsMcp.db" }); const exists = await fileExists(filePath, agentfs); if (exists) { return { @@ -79,7 +82,7 @@ const getServer = () => { inputSchema: writeSchemaShape, }, async ({ filePath, fileContent }) => { - const agentfs = await getAgentFS({}); + const agentfs = await getAgentFS({ filePath: "fsMcp.db" }); const success = await writeFile(filePath, fileContent, agentfs); if (success) { return { @@ -111,7 +114,7 @@ const getServer = () => { inputSchema: editSchemaShape, }, async ({ filePath, oldString, newString }) => { - const agentfs = await getAgentFS({}); + const agentfs = await getAgentFS({ filePath: "fsMcp.db" }); const editedContent = await editFile( filePath, oldString, @@ -148,9 +151,8 @@ const getServer = () => { inputSchema: listFilesSchemaShape, }, async () => { - const agentfs = await getAgentFS({}); + const agentfs = await getAgentFS({ filePath: "fsMcp.db" }); const files = await listFiles(agentfs); - console.log("Sending result: ", files) if (files != "") { return { content: [{ type: "text", text: files }] }; } else { @@ -166,90 +168,90 @@ const getServer = () => { } }, ); - return mcpServer -} + return mcpServer; +}; const app = createMcpExpressApp(); const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; -app.post('/mcp', async (req: Request, res: Response) => { - console.log('Received MCP request:', req.body); - try { - // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; +app.post("/mcp", async (req: Request, res: Response) => { + console.log("Received MCP request:", req.body); + try { + // Check for existing session ID + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request - use JSON response mode - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - enableJsonResponse: true, // Enable JSON response mode - onsessioninitialized: sessionId => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - } - }); + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request - use JSON response mode + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + enableJsonResponse: true, // Enable JSON response mode + onsessioninitialized: (sessionId) => { + // Store the transport by session ID when session is initialized + // This avoids race conditions where requests might come in before the session is stored + console.log(`Session initialized with ID: ${sessionId}`); + transports[sessionId] = transport; + }, + }); - // Connect the transport to the MCP server BEFORE handling the request - const server = getServer(); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - return; // Already handled - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided' - }, - id: null - }); - return; - } - - // Handle the request with existing transport - no need to reconnect - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error' - }, - id: null - }); - } + // Connect the transport to the MCP server BEFORE handling the request + const server = getServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + return; // Already handled + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: null, + }); + return; } + + // Handle the request with existing transport - no need to reconnect + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Internal server error", + }, + id: null, + }); + } + } }); // Handle GET requests for SSE streams according to spec -app.get('/mcp', async (req: Request, res: Response) => { - // Since this is a very simple example, we don't support GET requests for this server - // The spec requires returning 405 Method Not Allowed in this case - res.status(405).set('Allow', 'POST').send('Method Not Allowed'); +app.get("/mcp", async (req: Request, res: Response) => { + // Since this is a very simple example, we don't support GET requests for this server + // The spec requires returning 405 Method Not Allowed in this case + res.status(405).set("Allow", "POST").send("Method Not Allowed"); }); // Start the server const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); +app.listen(PORT, (error) => { + if (error) { + console.error("Failed to start server:", error); + process.exit(1); + } + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); }); // Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down server...'); - process.exit(0); -}); \ No newline at end of file +process.on("SIGINT", async () => { + console.log("Shutting down server..."); + process.exit(0); +});