mirror of
https://github.com/run-llama/agentfs-claude.git
synced 2026-07-01 21:24:01 -04:00
Merge pull request #8 from run-llama/clelia/add-support-for-codex
feat: add support for codex
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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.
|
||||
@@ -0,0 +1,7 @@
|
||||
[features]
|
||||
rmcp_client = true
|
||||
|
||||
[mcp_servers.filesystem]
|
||||
url = "http://localhost:3000/mcp"
|
||||
startup_timeout_sec = 30
|
||||
tool_timeout_sec = 30
|
||||
@@ -8,6 +8,7 @@
|
||||
"test": "rm -rf test.db* && vitest run --testTimeout=60000",
|
||||
"start": "pnpm exec tsx src/index.ts",
|
||||
"clean-start": "rm *.db* && pnpm exec tsx src/index.ts",
|
||||
"mcp-start": "pnpm exec tsx src/mcpServer.ts",
|
||||
"lint": "eslint ./src/ --fix",
|
||||
"format": "prettier --write ./src/",
|
||||
"build": "tsc",
|
||||
@@ -21,6 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^24.10.1",
|
||||
@@ -37,11 +39,18 @@
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.59",
|
||||
"@llamaindex/workflow-core": "^1.3.3",
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@openai/codex-sdk": "^0.73.0",
|
||||
"@visulima/colorize": "^1.4.29",
|
||||
"agentfs-sdk": "^0.2.1",
|
||||
"express": "^5.2.1",
|
||||
"figlet": "^1.9.4",
|
||||
"llama-cloud-services": "^0.4.3",
|
||||
"mime-types": "^3.0.2",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@openai/codex-sdk": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ export async function consoleInput(question: string): Promise<string> {
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const answer = await rl.question(question);
|
||||
const answer = await rl.question(bold(question));
|
||||
rl.close();
|
||||
return answer;
|
||||
}
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
import { Codex } from "@openai/codex-sdk";
|
||||
import type { ThreadEvent, Thread } from "@openai/codex-sdk";
|
||||
import {
|
||||
red,
|
||||
green,
|
||||
magentaBright,
|
||||
yellow,
|
||||
bold,
|
||||
cyan,
|
||||
} from "@visulima/colorize";
|
||||
|
||||
export const codex = new Codex({});
|
||||
|
||||
export async function runCodex(
|
||||
prompt: string,
|
||||
{ resumeSession = undefined }: { resumeSession: string | undefined },
|
||||
) {
|
||||
let thread: Thread | undefined = undefined;
|
||||
if (typeof resumeSession == "undefined") {
|
||||
thread = codex.startThread({
|
||||
skipGitRepoCheck: true,
|
||||
});
|
||||
} else {
|
||||
thread = codex.resumeThread(resumeSession, {
|
||||
skipGitRepoCheck: true,
|
||||
});
|
||||
}
|
||||
const { events } = await thread.runStreamed(prompt);
|
||||
|
||||
for await (const event of events) {
|
||||
switch (event.type) {
|
||||
case "thread.started":
|
||||
console.log(`Started session with ID: ${bold(event.thread_id)}`);
|
||||
break;
|
||||
case "item.started":
|
||||
await handleItemStart(event);
|
||||
break;
|
||||
case "item.updated":
|
||||
await handleItemUpdated(event);
|
||||
break;
|
||||
case "item.completed":
|
||||
await handleItemCompleted(event);
|
||||
break;
|
||||
case "turn.completed":
|
||||
await handleTurnCompletion(event);
|
||||
break;
|
||||
case "error":
|
||||
await handleError(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleItemStart(event: ThreadEvent) {
|
||||
if (event.type == "item.started") {
|
||||
if (event.item.type == "agent_message") {
|
||||
console.log(bold(magentaBright("Assistant started responding...")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "reasoning") {
|
||||
console.log(bold(magentaBright("Assistant started thinking...")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "mcp_tool_call") {
|
||||
console.log(
|
||||
bold(
|
||||
yellow(
|
||||
`Assistant started calling MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (event.item.error) {
|
||||
console.log(
|
||||
red(
|
||||
bold(
|
||||
`An error occurred while calling the tool: ${event.item.error.message}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (event.item.result) {
|
||||
let finalResult = "";
|
||||
for (const block of event.item.result.content) {
|
||||
if (block.type == "text") {
|
||||
finalResult += block.text + "\n";
|
||||
}
|
||||
}
|
||||
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
|
||||
}
|
||||
}
|
||||
} else if (event.item.type == "todo_list") {
|
||||
console.log(
|
||||
bold(green("Assistant started to produce a TODO list with items:")),
|
||||
);
|
||||
let c = 0;
|
||||
for (const i of event.item.items) {
|
||||
c += 1;
|
||||
console.log(
|
||||
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
|
||||
);
|
||||
}
|
||||
} else if (event.item.type == "web_search") {
|
||||
console.log(
|
||||
`Assistant started searching the web with query: ${event.item.query}`,
|
||||
);
|
||||
} else if (event.item.type == "command_execution") {
|
||||
console.log(
|
||||
`Assistant started to execute command: ${event.item.command}`,
|
||||
);
|
||||
} else if (event.item.type == "file_change") {
|
||||
console.log(
|
||||
bold(red("WARNING! The assistant is starting to change files:")),
|
||||
);
|
||||
for (const change of event.item.changes) {
|
||||
console.log(
|
||||
`The assistant would like to apply ${change.kind} to ${change.path}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(bold(red(`An error occurred: ${event.item.message}`)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleItemUpdated(event: ThreadEvent) {
|
||||
if (event.type == "item.updated") {
|
||||
if (event.item.type == "agent_message") {
|
||||
console.log(bold(magentaBright("Assistant updated its response...")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "reasoning") {
|
||||
console.log(bold(magentaBright("Assistant updated its thoughts...")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "mcp_tool_call") {
|
||||
console.log(
|
||||
bold(
|
||||
yellow(
|
||||
`Assistant updated its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (event.item.error) {
|
||||
console.log(
|
||||
red(
|
||||
bold(
|
||||
`An error occurred while calling the tool: ${event.item.error.message}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (event.item.result) {
|
||||
let finalResult = "";
|
||||
for (const block of event.item.result.content) {
|
||||
if (block.type == "text") {
|
||||
finalResult += block.text + "\n";
|
||||
}
|
||||
}
|
||||
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
|
||||
}
|
||||
}
|
||||
} else if (event.item.type == "todo_list") {
|
||||
console.log(bold(green("Assistant updated its TODO list with items:")));
|
||||
let c = 0;
|
||||
for (const i of event.item.items) {
|
||||
c += 1;
|
||||
console.log(
|
||||
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
|
||||
);
|
||||
}
|
||||
} else if (event.item.type == "web_search") {
|
||||
console.log(
|
||||
`Assistant updated its web search with query: ${event.item.query}`,
|
||||
);
|
||||
} else if (event.item.type == "command_execution") {
|
||||
console.log(`Assistant updated command execution: ${event.item.command}`);
|
||||
} else if (event.item.type == "file_change") {
|
||||
console.log(
|
||||
bold(red("WARNING! The assistant is starting to change files:")),
|
||||
);
|
||||
for (const change of event.item.changes) {
|
||||
console.log(
|
||||
`The assistant is updating the change it would like to apply (${change.kind}) to ${change.path}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(bold(red(`An error occurred: ${event.item.message}`)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleItemCompleted(event: ThreadEvent) {
|
||||
if (event.type == "item.completed") {
|
||||
if (event.item.type == "agent_message") {
|
||||
console.log(bold(magentaBright("Assistant completed its response:")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "reasoning") {
|
||||
console.log(bold(magentaBright("Assistant completed its thoughts:")));
|
||||
console.log(event.item.text);
|
||||
} else if (event.item.type == "mcp_tool_call") {
|
||||
console.log(
|
||||
bold(
|
||||
yellow(
|
||||
`Assistant completed its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (event.item.error) {
|
||||
console.log(
|
||||
red(
|
||||
bold(
|
||||
`An error occurred while calling the tool: ${event.item.error.message}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (event.item.result) {
|
||||
let finalResult = "";
|
||||
for (const block of event.item.result.content) {
|
||||
if (block.type == "text") {
|
||||
finalResult += block.text + "\n";
|
||||
}
|
||||
}
|
||||
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
|
||||
}
|
||||
}
|
||||
} else if (event.item.type == "todo_list") {
|
||||
console.log(bold(green("Assistant completed its TODO list with items:")));
|
||||
let c = 0;
|
||||
for (const i of event.item.items) {
|
||||
c += 1;
|
||||
console.log(
|
||||
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
|
||||
);
|
||||
}
|
||||
} else if (event.item.type == "web_search") {
|
||||
console.log(
|
||||
`Assistant updated its web search with query: ${event.item.query}`,
|
||||
);
|
||||
} else if (event.item.type == "command_execution") {
|
||||
console.log(`Assistant updated command execution: ${event.item.command}`);
|
||||
} else if (event.item.type == "file_change") {
|
||||
console.log(
|
||||
bold(red("ERROR! The assistant has completed its file changes:")),
|
||||
);
|
||||
for (const change of event.item.changes) {
|
||||
console.log(
|
||||
`The assistant has completed the change it applied to ${change.path} (${change.kind})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(bold(red(`An error occurred: ${event.item.message}`)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTurnCompletion(event: ThreadEvent) {
|
||||
if (event.type == "turn.completed") {
|
||||
console.log(cyan(bold("Turn completed, usage:")));
|
||||
console.log(`Input tokens: ${event.usage.input_tokens}`);
|
||||
console.log(`Cached input tokens: ${event.usage.cached_input_tokens}`);
|
||||
console.log(`Output tokens: ${event.usage.output_tokens}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleError(event: ThreadEvent) {
|
||||
if (event.type == "error") {
|
||||
console.log(bold(red(`An error occurred: ${event.message}`)));
|
||||
}
|
||||
}
|
||||
+59
-16
@@ -4,9 +4,10 @@ import { recordFiles } from "./filesystem";
|
||||
import { getAgentFS } from "./mcp";
|
||||
import { Agent } from "./claude";
|
||||
import { queryOptions } from "./options";
|
||||
import { bold } from "@visulima/colorize";
|
||||
import { bold, green, red } from "@visulima/colorize";
|
||||
import { consoleInput, renderLogo } from "./cli";
|
||||
import * as fs from "fs";
|
||||
import { runCodex } from "./codex";
|
||||
|
||||
async function main() {
|
||||
const { withState } = createStatefulMiddleware(() => ({}));
|
||||
@@ -15,6 +16,7 @@ async function main() {
|
||||
const filesRegisteredEvent = workflowEvent<void>();
|
||||
const requestPromptEvent = workflowEvent<void>();
|
||||
const promptEvent = workflowEvent<{
|
||||
chosenAgent: string;
|
||||
prompt: string;
|
||||
resume: string | undefined;
|
||||
plan: boolean;
|
||||
@@ -28,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;
|
||||
@@ -43,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();
|
||||
}
|
||||
});
|
||||
@@ -51,7 +61,9 @@ async function main() {
|
||||
workflow.handle([filesRegisteredEvent], async (_context, _event) => {
|
||||
console.log(
|
||||
bold(
|
||||
"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();
|
||||
@@ -59,15 +71,25 @@ async function main() {
|
||||
|
||||
workflow.handle([promptEvent], async (_context, event) => {
|
||||
const prompt = event.data.prompt;
|
||||
const agent = new Agent(queryOptions, {
|
||||
resume: event.data.resume,
|
||||
plan: event.data.plan,
|
||||
});
|
||||
try {
|
||||
await agent.run(prompt);
|
||||
return stopEvent.with({ success: true, error: null });
|
||||
} catch (error) {
|
||||
return stopEvent.with({ success: false, error: JSON.stringify(error) });
|
||||
if (event.data.chosenAgent == "claude") {
|
||||
const agent = new Agent(queryOptions, {
|
||||
resume: event.data.resume,
|
||||
plan: event.data.plan,
|
||||
});
|
||||
try {
|
||||
await agent.run(prompt);
|
||||
return stopEvent.with({ success: true, error: null });
|
||||
} catch (error) {
|
||||
return stopEvent.with({ success: false, error: JSON.stringify(error) });
|
||||
}
|
||||
} 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) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,6 +97,13 @@ async function main() {
|
||||
sendEvent(startEvent.with({ workingDirectory: "./" }));
|
||||
await stream.until(requestPromptEvent).toArray();
|
||||
const snapshotData = await snapshot();
|
||||
let agentOfChoice = "claude";
|
||||
const chosenAgent = await consoleInput(
|
||||
"What agent would you like to use? [codex/claude] ",
|
||||
);
|
||||
if (chosenAgent.trim() != "") {
|
||||
agentOfChoice = chosenAgent;
|
||||
}
|
||||
const humanResponse = await consoleInput("Your prompt: ");
|
||||
console.log(
|
||||
bold("Would you like to resume a previous session? Leave blank if not"),
|
||||
@@ -84,21 +113,35 @@ async function main() {
|
||||
if (resumeSession.trim() != "") {
|
||||
sessionId = resumeSession;
|
||||
}
|
||||
console.log(bold("Would you like to activate plan mode? [y/n]"));
|
||||
const activatePlan = await consoleInput("Your answer: ");
|
||||
let planMode = false;
|
||||
if (["yes", "y", "yse"].includes(activatePlan.trim().toLowerCase())) {
|
||||
planMode = true;
|
||||
if (agentOfChoice == "claude") {
|
||||
console.log(bold("Would you like to activate plan mode? [y/n]"));
|
||||
const activatePlan = await consoleInput("Your answer: ");
|
||||
if (["yes", "y", "yse"].includes(activatePlan.trim().toLowerCase())) {
|
||||
planMode = true;
|
||||
}
|
||||
}
|
||||
const resumedContext = workflow.resume(snapshotData);
|
||||
resumedContext.sendEvent(
|
||||
promptEvent.with({
|
||||
chosenAgent: agentOfChoice,
|
||||
prompt: humanResponse,
|
||||
resume: sessionId,
|
||||
plan: planMode,
|
||||
}),
|
||||
);
|
||||
await resumedContext.stream.until(stopEvent).toArray();
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch(console.error);
|
||||
|
||||
+5
-5
@@ -10,16 +10,16 @@ import {
|
||||
} from "./filesystem";
|
||||
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
const readSchemaShape = {
|
||||
export const readSchemaShape = {
|
||||
filePath: z.string().describe("Path of the file to read"),
|
||||
};
|
||||
|
||||
const writeSchemaShape = {
|
||||
export const writeSchemaShape = {
|
||||
filePath: z.string().describe("Path of the file to write"),
|
||||
fileContent: z.string().describe("Content of the file"),
|
||||
};
|
||||
|
||||
const editSchemaShape = {
|
||||
export const editSchemaShape = {
|
||||
filePath: z.string().describe("Path of the file to write"),
|
||||
oldString: z.string().describe("Old string in the file (to replace)"),
|
||||
newString: z
|
||||
@@ -27,13 +27,13 @@ const editSchemaShape = {
|
||||
.describe("New string with which to replace the old string"),
|
||||
};
|
||||
|
||||
const fileExistsSchemaShape = {
|
||||
export const fileExistsSchemaShape = {
|
||||
filePath: z
|
||||
.string()
|
||||
.describe("Path of the file whose existence has to be checked"),
|
||||
};
|
||||
|
||||
const listFilesSchemaShape = {};
|
||||
export const listFilesSchemaShape = {};
|
||||
|
||||
// eslint-disable-next-line
|
||||
const readSchema = z.object(readSchemaShape);
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
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 {
|
||||
readSchemaShape,
|
||||
fileExistsSchemaShape,
|
||||
writeSchemaShape,
|
||||
listFilesSchemaShape,
|
||||
editSchemaShape,
|
||||
getAgentFS,
|
||||
} from "./mcp";
|
||||
import {
|
||||
readFile,
|
||||
writeFile,
|
||||
editFile,
|
||||
fileExists,
|
||||
listFiles,
|
||||
} from "./filesystem";
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
|
||||
const getServer = () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "filesystem-mcp",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
mcpServer.registerTool(
|
||||
"read_file",
|
||||
{
|
||||
description: "Read a file by passing its path.",
|
||||
inputSchema: readSchemaShape,
|
||||
},
|
||||
async ({ filePath }) => {
|
||||
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
|
||||
const content = await readFile(filePath, agentfs);
|
||||
if (typeof content == "string") {
|
||||
return { content: [{ type: "text", text: content }] };
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Could not read ${filePath}. Please check that the file exists and submit the request again.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
"file_exists",
|
||||
{
|
||||
description: "Check whether a file exists or not by passing its path.",
|
||||
inputSchema: fileExistsSchemaShape,
|
||||
},
|
||||
async ({ filePath }) => {
|
||||
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
|
||||
const exists = await fileExists(filePath, agentfs);
|
||||
if (exists) {
|
||||
return {
|
||||
content: [{ type: "text", text: `File ${filePath} exists` }],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{ type: "text", text: `File ${filePath} does not exist.` }],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
"write_file",
|
||||
{
|
||||
description: "Write a file by passing its path and content.",
|
||||
inputSchema: writeSchemaShape,
|
||||
},
|
||||
async ({ filePath, fileContent }) => {
|
||||
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
|
||||
const success = await writeFile(filePath, fileContent, agentfs);
|
||||
if (success) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `File ${filePath} successfully written with content:\n\n'''\n${fileContent}\n'''`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `There was an error while writing file ${filePath}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
"edit_file",
|
||||
{
|
||||
description:
|
||||
"Edit a file by passing its path, the old string and the new string.",
|
||||
inputSchema: editSchemaShape,
|
||||
},
|
||||
async ({ filePath, oldString, newString }) => {
|
||||
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
|
||||
const editedContent = await editFile(
|
||||
filePath,
|
||||
oldString,
|
||||
newString,
|
||||
agentfs,
|
||||
);
|
||||
if (typeof editedContent == "string") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully edited ${filePath}. New content:\n\n'''\n${editedContent}\n'''`,
|
||||
},
|
||||
],
|
||||
} as CallToolResult;
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Could not edit ${filePath}. Please check that the file exists and submit the request again.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
"list_files",
|
||||
{
|
||||
description: "List all the available files",
|
||||
inputSchema: listFilesSchemaShape,
|
||||
},
|
||||
async () => {
|
||||
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
|
||||
const files = await listFiles(agentfs);
|
||||
if (files != "") {
|
||||
return { content: [{ type: "text", text: files }] };
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Could not list files. Please report this failure to the user`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
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;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
// 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}`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("Shutting down server...");
|
||||
process.exit(0);
|
||||
});
|
||||
+2
-2
@@ -131,9 +131,9 @@ const hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>> = {
|
||||
};
|
||||
|
||||
export const systemPrompt = `
|
||||
You are an expert programmer whose task is to assist the user implement their requsts within the current working directory.
|
||||
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, wich provides the following tools:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user