{"id":30282716,"url":"https://github.com/TomasHubelbauer/bun-mcp","last_synced_at":"2025-08-16T17:04:09.848Z","repository":{"id":296605220,"uuid":"993834845","full_name":"TomasHubelbauer/bun-mcp","owner":"TomasHubelbauer","description":"A script-based local MCP server demonstration. The server is built in Bun and runs in VS Code Copilot Chat in agent mode.","archived":false,"fork":false,"pushed_at":"2025-06-17T12:09:08.000Z","size":829,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-06T03:41:01.427Z","etag":null,"topics":["agent-mode","bun","copilot","copilot-chat","mcp","mcp-server","vs-code","vscode","vscode-copilot-chat"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TomasHubelbauer.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-31T16:15:12.000Z","updated_at":"2025-07-17T20:47:50.000Z","dependencies_parsed_at":"2025-06-01T08:30:20.385Z","dependency_job_id":"8f5099d5-6189-493e-ae5e-938863e59267","html_url":"https://github.com/TomasHubelbauer/bun-mcp","commit_stats":null,"previous_names":["tomashubelbauer/bun-mcp"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TomasHubelbauer/bun-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fbun-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fbun-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fbun-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fbun-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TomasHubelbauer","download_url":"https://codeload.github.com/TomasHubelbauer/bun-mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fbun-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270742043,"owners_count":24637504,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-16T02:00:11.002Z","response_time":91,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-mode","bun","copilot","copilot-chat","mcp","mcp-server","vs-code","vscode","vscode-copilot-chat"],"created_at":"2025-08-16T17:02:15.825Z","updated_at":"2025-08-16T17:04:09.834Z","avatar_url":"https://github.com/TomasHubelbauer.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Bun MCP\n\nThe goal of this repository is to prototype an [MCP (model context protocol)](https://modelcontextprotocol.io)\nserver using the [Bun](https://bun.sh) runtime and use its tools through the VS\nCode Copilot chat agent mode.\n\n![A screen recording showing the resulting MCP server in action](demo.gif)\n\nI started off by reading though the Model Context Protocol documentation.\nIn the nomenclature of the MCP documentation, these will be the elements we use:\n\n- Host with client: VS Code with Copilot chat in agent mode\n- Server: the script implemented in this repository\n\nFor this setup, the documentation has a specific learning path:\n\n[For Server Developers](https://modelcontextprotocol.io/quickstart/server)\n\nMCP servers work by exposing tools which are akin to REST endpoints and the LLM\nused by the MCP client can invoked these tools if it determines the user prompt\ncould benefit from data this tools are able to provide.\n\nThe documentation page provides a sample weather service MCP server, which is a\ngood example as LLMs are not capable of providing weather information given the\nfact that weather forecast is a live datum and thus not possible to infer by the\nLLM.\n\nI will build an MCP server capable of managing a to-do list in a MarkDown file.\nThe goal of the MCP server is to give the user the tools required to created and\nmaintain this document by allowing them to add, toggle and remove to-do items.\n\nA thing worth mentioning is that MCP servers can provide different types of data\nto the MCP clients:\n\n- Resources: contents of files and data blobs\n- Tools: methods for the LLM to invoke and use the result of\n- Prompts: pre-defined prompts to help improve user prompts for best results\n\nAs in the documentation page, my MCP server will focus on providing tools only.\n\nUnlike the documentation page, I will be using Bun and not Node.\nThat being said I will probably still be able to benefit from the reference\nimplementation [here](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript).\n\nThe tutorial page includes a server implementation based around an SDK package.\nI will take that route first, but down the line, I would like to build a server\nfrom scratch, implementing the raw protocol.\n\nAs I understand it, this should not be very difficult as the servers are capable\nof communicating over standard I/O as well as server-sent events, both primitive\nto implement without any dependencies.\n\n- [ ] Drop the SDK package and rebuild the server with direct MCP protocol\n\nUntil them, I am starting off by adding the `@modelcontextprotocol/sdk` package\nas a dependency using `bun add @modelcontextprotocol/sdk`.\n\nBun will create the `package.json` file and as with all my personal projects, I\nam changing the version to `latest` so that the project doesn't get stuck on an\nold version.\n\n`package.json`:\n\n```json\n{\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"latest\"\n  }\n}\n```\n\nI am also adding a `.gitignore` file and ignoring `node_modules` and `bun.lock`.\nI do not do dependency vendoring and I do not require a lock file as all the\ndependencies should always install at their latest version no matter what.\n\nThe meat and potatoes of the server will live in a new file named `index.ts`.\nI assume in the client configuration I will be able to specify the server via a\ncommand and an argument, which if it is the case, will make it really easy by\njust specifying `bun `. as the command and argument pair.\n\nMy script will be ESM and will have no build steps as per the usual.\nIt will use TypeScript via Bun's native TypeScript support and won't override\nBun's default TypeScript configuration.\n\nThis basic MCP SDK `import` and server manifest specification look like this:\n\n`index.ts`:\n\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nconst server = new McpServer({\n  name: \"to-do\",\n  version: \"0.0.0\",\n  capabilities: {\n    tools: {},\n  },\n});\n```\n\nThe next step is to register tools for the MCP server to expose.\nI will start with one - `list-todos` and it will return hard-coded data for now.\n\n```typescript\nserver.tool(\"list-todos\", \"Lists all to-do items in the TODO.md file\", () =\u003e {\n  return {\n    content: [\n      {\n        type: \"text\",\n        text: \"- [ ] Do the dishes\\n- [ ] Go grocery shopping\\n- [ ] Plan the weekend trip\",\n      },\n    ],\n  };\n});\n```\n\nWith an tool defined, it is time to configure the server transport so that the\nclients know how to talk to the server.\nThis requires a new `import` atop the file, I am choosing to use the standard\nI/O for this.\n\n```typescript\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\n// …\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\nAt this point, it is time to test the MCP server.\nThe documentation page demonstrates how to do this with Claude so I will defer\nelsewhere to learn how to configure this in VS Code.\n\nThe VS Code documentation on MCP lists several options:\nhttps://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server\n\nI much prefer the workspace configuration option, so I have created an empty\n`.vscode/mcp.json` file in the workspace of this repository and opened it in a\nnew VS Code tab.\n\nVS Code recognizes this special path and displays a button titled Add Server at\nthe bottom right corner of the file editor area.\n\nI clicked the button and selected the Command (stdio) option.\n\nAt this point, a bit of fiddling and experimentation had to take place.\nThe command I tried first was just `bun`, but I didn't realize this flow would\nnot ask me for an argument for the command.\nThe only other thing it asked about was the server name, where I put `to-do`.\n\nThe server failed to start and in the Output pane in the MCP: to-do channel I\ncould see why - the VS Code host executed the command and started interpreting\nits standard I/O streams for transport.\n\nBut since the command was just `bun`, it printed its default output when no\nscript is provided or discovered.\nThese lines failed to parse as MCP messages, so the connection failed to be\nestablished.\n\nThis is the incorrect configuration:\n\n```json\n{\n  \"servers\": {\n    \"to-do\": {\n      \"type\": \"stdio\",\n      \"command\": \"bun\",\n      \"args\": []\n    }\n  }\n}\n```\n\nAt this point I started tweaking the JSON code itself instead of relying on the\nVS Code UI flow.\n\nI figured a simple fix could be to change the command to `bun .` so that Bun was\ninvoked and attempted to start `index.ts`.\n\nI hit the Restart code lens that VS Code put atop the server entry.\nThis resulted in an error, again.\n\nIt seems that MCP servers that are defined as commands are not run in the\nworkspace directory.\nTo verify this, I changed the command to `pwd` and re-ran it.\nThis printed `/Users/tom` confirming my suspicion.\n\nFrom here there are multiple options.\nI do not want to hard-code the full path of the script as the argument to Bun,\nso I think I could rely on VS Code configuration substitutions:\nhttps://code.visualstudio.com/docs/reference/variables-reference\n\nI changed the command to `echo '${workspaceFolder}'` to see if it would print\nthe value of the variable in the Output window and hit Restart again.\n\nThis printed the expected directory!\nOf course the server still did not start, but at this point I knew the final\ncommand would have to be `bun ${workspaceFolder}`.\nI was surprised to see this not work either, but I soon realized I should pass\nthe `workspaceFolder` variable as an argument in the `args` field instead.\n\nI changed the command to `echo` once again to make sure everything would still\nwork as expected:\n\n```json\n{\n  \"servers\": {\n    \"to-do\": {\n      \"type\": \"stdio\",\n      \"command\": \"echo\",\n      \"args\": [\"${workspaceFolder}\"]\n    }\n  }\n}\n```\n\nThis printed `[warning] Failed to parse message: \"/Users/tom/Desktop/bun-mcp\\n\"`\nletting me know the variable substitution and command passing worked.\n\nI could change the `command` to `bun` now and get the server to start up.\nThe code lens atop the server entry changed to say \"Running…\" and I saw a flurry\nof activity in the Output tab as well:\n\n```\n[info] Connection state: Starting\n[info] Starting server from LocalProcess extension host\n[info] Connection state: Starting\n[info] Connection state: Running\n[info] Discovered 1 tools\n```\n\nThis means my MCP server and its sole tool are discovered and I can try a prompt\nthat should trigger it in the VS Code GitHub Copilot chat pane in agent mode.\n\nI opened GitHub Copilot chat using the Copilot icon to the right of the command\nbar in the top center of the VS Code window.\n\nIt starts off the in Ask mode so I toggled that switch to Agent.\nThis brings some new icons to the chat composer: Start Voice Chat, Select Tools\nand Discover Tools / New Tools Available (hidden if not applicable).\n\nI clicked on the Select Tools button and checked the list that opened to see the\n`to-do` server was listed and checked along with its `list-todos` tool!\n\nThis means my prompt should be able to use this tool now.\nI asked Copilot:\n\n\u003e What todos do I have on my list?\n\nIt was either not smart enough or too smart, because it used the checkbox list\nin this README file (which was automatically included as a reference in the chat\nas evidenced by the item atop the prompt input area) and answered by listing the\nto-do checkboxes in it.\n\nI clicked the eye icon next to `readme.md` to remove the current file reference\nfrom the prompt context and re-ran the same prompt.\n\nThis time, Copilot Chat asked me whether I want to allow the `to-do` MCP server\nto run the `list-todos` tool with a Continue and Cancel buttons presented.\nI selected the chevron next to the Continue button and selected Always Allow in\nThis Workspace so that my MCP server would be allowed to run uninterrupted in\nthis repository directory from now on.\n\nThe Copilot Chat changed to indicate it ran the `list-todos` tool of my `to-do`\nserver and responded with:\n\n\u003e Your current to-do list contains the following items:\n\u003e `\u003cinput disabled=\"\" type=\"checkbox\"\u003e` Do the dishes\n\u003e `\u003cinput disabled=\"\" type=\"checkbox\"\u003e` Go grocery shopping\n\u003e `\u003cinput disabled=\"\" type=\"checkbox\"\u003e` Plan the weekend trip\n\u003e Let me know if you want to add, remove, or update any of these tasks!\n\nAmusingly, the MarkDown checkboxes do not render in the Copilot chat UI so I\nthink I will have to go with emoji when I am changing the tool to not return\nhard-coded data.\n\nAlso, Copilot is confident it can help me edit this list despite my MCP server\nnot exposing tools for doing that yet.\nI'll chalk that one up to hallucinations/insufficiently intelligent prompt for\nthe MCP server interaction.\n\nFor the next step, I will add a tool for creating a new to-do item and change\nthe existing tool for listing to-do items to use an in-memory storage so that I\ncan add and list this new to-do item.\n\nWhen it comes to registering tools with arguments, the MCP SDK seems to be tied\nto Zod for schema validation of the structure of arguments to the MCP tools.\nI do not prefer this and in my future implementation where I implement the MCP\nprotocol by hand, I will drop the Zod dependency, but for now I will oblige and\nuse it:\n\n`bun add zod`\n\n`package.json`:\n\n```json\n{\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"latest\",\n    \"zod\": \"lates\"\n  }\n}\n```\n\nHere's how I changed `index.ts` to accomplish this:\n\n```typescript\nimport { z } from \"zod\";\n\n// …\n\nconst todos: { name: string; isChecked: boolean }[] = [];\n\nserver.tool(\"list-todos\", \"Lists all to-do items in the TODO.md file\", () =\u003e {\n  return {\n    content: [\n      {\n        type: \"text\",\n        text: todos\n          .map((todo) =\u003e `${todo.isChecked ? \"✅\" : \"❎\"} ${todo.name}`)\n          .join(\"\\n\"),\n      },\n    ],\n  };\n});\n\nserver.tool(\n  \"add-todo\",\n  \"Adds a new to-do item to the TODO.md file\",\n  {\n    name: z.string().describe(\"The name of the to-do item\"),\n  },\n  ({ name }) =\u003e {\n    const newTodo = { name, isChecked: false };\n    todos.push(newTodo);\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Added new to-do item: ${newTodo.name}`,\n        },\n      ],\n    };\n  }\n);\n\n// …\n```\n\nAfter making these changes, I went to the `mcp.json` tab again and pressed the\nRestart code lens action to make the changes take an effect.\n\nThe other code lens item changed to say \"2 tools\" indicating the changes did\nindeed take place further corroborated by the log of the Output pane in the\nMCP: to-do channel which now added these lines:\n\n```\n[info] Stopping server to-do\n[info] Connection state: Stopped\n[info] Starting server to-do\n[info] Connection state: Starting\n[info] Starting server from LocalProcess extension host\n[info] Connection state: Starting\n[info] Connection state: Running\n[info] Discovered 2 tools\n```\n\nI kept the current file reference outside of the Copilot chat context and asked\nit to add a new to-do item for me:\n\n\u003e Add a todo to buy groceries for dinner\n\nI was again prompted to either Continue or Cancel the `to-do` MCP server call,\nthis time for the `add-todo` tool, which I found surprising and I guess this\ncheck is presented on a per-tool basis as opposed to per-server basis as I\ninitially expected.\n\nI again chose the Always in this Workspace option in the Continue dropdown.\n\nCopilot responded with:\n\n\u003e A new to-do item \"Buy groceries for dinner\" has been added to your list.\n\u003e If you need to view, update, or remove any to-dos, just let me know!\n\nI expanded the \"Ran `add-todo`\" box atop the Copilot response to drill down.\nIt printed the input it crafted based on the prompt as well as the output from\nthe MCP server.\n\nInput:\n\n```json\n{\n  \"name\": \"Buy groceries for dinner\"\n}\n```\n\nOutput:\n\n\u003e Added new to-do item: Buy groceries for dinner\n\nNext I asked what todos were on my list:\n\n\u003e What todos do I have on my list?\n\nThis time, I wasn't prompted to allow the tool run, it ran without interruption,\nso the prompt really seems to be per-tool.\n\nCopilot responded with:\n\n\u003e Your current to-do list contains:\n\u003e `\u003cinput disabled=\"\" type=\"checkbox\"\u003e` Buy groceries for dinner\n\u003e Let me know if you want to add, remove, or update any to-dos!\n\nInspecting the tool call box for details:\n\nInput:\n\n```\n{}\n```\n\nOutput:\n\n\u003e ❎ Buy groceries for dinner\n\nSo it seems the Copilot LLM took this tool response and reformated it to a\nMarkDown checklist on its own which means we've run afoul of the broken MarkDown\nrendering in the VS Code Copilot Chat again.\n\nI am not sure if this can be prevented with tools along, but the MCP server\ncould potentially expose a prompt that would reword the simple query for the\nto-do list view into a richer query instructing Copilot to format the list in a\nparticular way, possibly using the emojis I used in the code instead of a\nMarkDown checklist.\n\n- [ ] Expose a prompt to enrich user prompts to avoid MarkDown list rendering\n\nTwo things are left to make the to-do list management flow complete: toggling\nand removing to-do items.\n\nI added a tool for toggling to-do items based on a keyword like so:\n\n```typescript\nserver.tool(\n  \"toggle-todo\",\n  \"Toggles the completion status of a to-do item\",\n  {\n    keyword: z\n      .string()\n      .describe(\"A keyword in the name of the to-do item to toggle\"),\n  },\n  ({ keyword }) =\u003e {\n    const todo = todos.find((todo) =\u003e todo.name.includes(keyword));\n    if (!todo) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `To-do item containing \"${keyword}\" not found.`,\n          },\n        ],\n      };\n    }\n\n    todo.isChecked = !todo.isChecked;\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Toggled to-do item \"${todo.name}\" to ${\n            todo.isChecked ? \"completed\" : \"not completed\"\n          }.`,\n        },\n      ],\n    };\n  }\n);\n```\n\nI restarted the MCP server in VS Code and asked Copilot:\n\n\u003e Cross the dinner to-do item off\n\nI confirmed Always in this Workspace for the `toggle-todo` tool call and go this\nreply:\n\n\u003e It appears that the to-do item containing \"dinner\" was not found.\n\u003e Could you please confirm the exact wording of the to-do, or let me know if it\n\u003e was recently changed or removed?\n\nI realized the issue was the MCP server restart losing the in-memory storage\nstate, so another thing was left to do, actually: persistence.\n\nI planned on replaced the `todos` constant with I/O helpers to read and write\nthe to-do items to the storage.\n\nTo implement these helpers, I used Bun's I/O methods which required me to add\nBun types to the dependencies so the I/O methods were accessible in TypeScript:\n\n`bun add -D @types/bun`\n\n`package.json`:\n\n```json\n{\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"latest\",\n    \"zod\": \"latest\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\"\n  }\n}\n```\n\nI imported Bun using `import Bun from \"bun\";` and realized that in order to be\nable to write helpers for I/O, I would need to make sure the script was aware of\nthe directory path to use.\n\nI assumed that the working directory of the script was `/Users/tom` because we\nare invoking `bun` with a full path to the VS Code workspace precisely because\nit starts off in that directory.\n\nI decided to still verify this by changing the default `todos` to list the full\npath of the directory the script was running in:\n\n```typescript\nconst todos: { name: string; isChecked: boolean }[] = [\n  {\n    name: `Know that the working directory is ${import.meta.dirname}`,\n    isChecked: true,\n  },\n];\n```\n\nI restarted the server and asked Copilot what my todos were and it said:\n\n\u003e Your current to-do list contains:\n\u003e `\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e` Know that the working directory is /Users/tom/Desktop/bun-mcp\n\u003e Let me know if you want to add, remove, or update any to-dos!\n\nSo, surprisingly (to me), if a script is invoked via a directory path I guess it\nsets the working directory to that path.\nThis simplifies affairs a bit.\n\nI replaced the `todos` constant with this code:\n\n```typescript\nconst FILE_PATH = \"TODO.md\";\n\nasync function readTodos() {\n  const text = await Bun.file(FILE_PATH).text();\n  const lines = text.split(\"\\n\").filter((line) =\u003e line.trim() !== \"\");\n  return lines.map((line) =\u003e {\n    const isChecked = line.startsWith(\"- [x] \");\n    const name = line.slice(\"- [?] \".length).trim();\n    return { name, isChecked };\n  });\n}\n\nasync function writeTodos(todos: Awaited\u003cReturnType\u003ctypeof readTodos\u003e\u003e) {\n  const content = todos\n    .map((todo) =\u003e `- [${todo.isChecked ? \"x\" : \" \"}] ${todo.name}`)\n    .join(\"\\n\");\n  await Bun.write(FILE_PATH, content);\n}\n```\n\nI also updated the three existing tools to make their callback methods `async`\nand add `const todos = await readTodos()` to make the usages of `todos` work\nagain and add `await writeTodos(todos)` whenever a to-do item was mutated to\npersist the changes to the file.\n\nI restarted the MCP server and started off with this prompt:\n\n\u003e Add taking out the bin to my todo list\n\nThe output of the tool call said this:\n\n\u003e ENOENT: no such file or directory, open 'TODO.md'\n\nI didn't remember that `Bun.file().text` would throw on a non-existent file and\nAFAIK there is no mechanism to provide fall-back content so I changed the I/O\nhelper methods like so:\n\n```typescript\nconst FILE = Bun.file(\"TODO.md\");\n\nasync function readTodos() {\n  const text = (await FILE.exists()) ? await FILE.text() : \"\";\n  const lines = text.split(\"\\n\").filter((line) =\u003e line.trim() !== \"\");\n  return lines.map((line) =\u003e {\n    const isChecked = line.startsWith(\"- [x] \");\n    const name = line.slice(\"- [?] \".length).trim();\n    return { name, isChecked };\n  });\n}\n\nasync function writeTodos(todos: Awaited\u003cReturnType\u003ctypeof readTodos\u003e\u003e) {\n  const content = todos\n    .map((todo) =\u003e `- [${todo.isChecked ? \"x\" : \" \"}] ${todo.name}`)\n    .join(\"\\n\");\n  await Bun.write(FILE, content);\n}\n```\n\nI restarted the MCP server and tried again by pressing the up-arrow key in the\nchat composer to recall the last message that was sent and resending it.\n\nGitHub Copilot was too smart again, because it created `TODO.md` on its own and\nadded the MarkDown checkbox to it without calling my MCP server.\n\nI reworded the prompt to hopefully hit my MCP server:\n\n\u003e Add a new to-do item to my to-do list for taking out the trash\n\nThis resulted in the exact same thing happening so I instead asked:\n\n\u003e What items are on my to-do list?\n\nThis invoked by MCP server correctly.\n\n\u003e Your to-do list is currently empty.\n\u003e If you would like to add a new item, just let me know what you'd like to include!\n\nI followed-up with:\n\n\u003e Add an item for taking out the trash\n\nThis didn't work either so I used the Select Tools button again and unchecked\nthe Codebase option under GitHub Copilot Chat hoping it would prevent it from\nbeing able to write files, but it didn't work, so I turned it back on.\n\nI resorted to this prompt:\n\n\u003e Add a todo using my to-do list MCP server for taking out the trash\n\nThis invoked the right tool correctly, but I didn't see `TODO.md` be created so\nI asked what files were on my to-do list to learn that it was empty.\n\nI do not know how to debug MCP server scripts yet, so I resorted to more debug\ninformation smuggling via tool results, but this time I realized I should have\nused `process.cwd()` instead of `import.meta.dir` the whole time!\n\n- [ ] Figure out how to run MCP server scripts under VS Code debugger\n\nI scraped the idea of adding more information to the tools and instead went\nstraight to the Terminal app and ran `cd ~` followed by `ls`.\n\nThis let me see the `TODO.md` file in my macOS user directory.\nI ran `cat TODO.md` and saw the new to-do item there.\n\nThis means the file is being written correctly (albeit missing the EOF newline),\nwhich raises the question of why I am not seeing the new to-do item in the reply\nto `list-todos`.\n\nBut, first things first, I needed to find a way to tell the MCP server what the\nworkspace directory was and it couldn't me `import.meta.dir`, because this case\nwhere the workspace directory of the workspace where I am using GitHub Copilot\nis the same as the workspace directory where I am developing it as a special\ncase that is not going to hold for other workspaces.\n\nI changed the `args` line in `mcp.json` to this:\n\n```json\n\"args\": [\"${workspaceFolder}\", \"${workspaceFolder}\"]\n```\n\nThis way I can use `process.argv` and get the sole argument of the script (aside\nof the script file itself) and derive the working directory from that.\n\nIt is not ideal that the MCP server needs to be installed in a way where the\nworkspace directory is passed explicitly, it should be able to ask for this type\nof information itself, but presently I do not know of a way to do that or if it\neven is supported in the MCP protocol.\n\nFrom a quick web search, it appears the answer may be that it isn't.\nI found some mentions of `process.env.WORKSPACE_FOLDER_PATHS` which is not there\nin my MCP server's `process.env` and I also found a note that `.` expands to the\nworkspace root directory in the `mcp.json` `args` configuration, which I tested\nand it didn't seem to work either.\n\nFor now I will stick with the double-`workspaceFolder` in `args.`\n\n- [ ] File a VS Code issue asking to pass workspace root to MCP server scripts\n\nI added a check at the top of the MCP server script to guide users and to change\nthe working directory if it is correctly provided:\n\n```typescript\nif (!process.argv[2]) {\n  throw new Error(\n    'Register the MCP server with `args` set to `[\"${workspaceFolder}\", \"${workspaceFolder}\"]`.'\n  );\n}\n\nprocess.chdir(process.argv[2]);\n```\n\nWith these changes, I should now be able to re-run the specific prompt asking to\nmake a new todo and see the `TODO.md` file appear created by my MCP server, not\nthe Copilot agent itself.\n\nI restarted the server and tried.\n\n\u003e Add a todo using my to-do list MCP server for taking out the trash\n\nI saw `TODO.md` and it had the right contents:\n\n```markdown\n- [ ] Take out the trash\n```\n\nAt this point I realized for this particular repository, since the `TODO.md`\nfile is a part of testing data, essentially, I should ignore it.\nIt should only be left unignored in workspaces other than this one that happen\nto use the MCP server.\n\n`.gitignore`:\n\n```shell\nTODO.md\n\n# NPM\nnode_modules\n\n# Bun\nbun.lock\n```\n\nI tried the \"What's on my todo list?\" prompt again and it replied saying there\nwere no todos.\nI suspected the `const` with the `Bun.File` instance was probably caching\nsomething and I should re-create it in each call to `readTodos`.\nThis suspicion was made stronger by the fact when I restarted the MCP server\nwith no changes, the to-do item was now listed.\n\nI changed the I/O helpers like so:\n\n```typescript\nconst FILE_NAME = \"TODO.md\";\n\nasync function readTodos() {\n  const text = (await Bun.file(FILE_NAME).exists())\n    ? await Bun.file(FILE_NAME).text()\n    : \"\";\n\n  const lines = text.split(\"\\n\").filter((line) =\u003e line.trim() !== \"\");\n  return lines.map((line) =\u003e {\n    const isChecked = line.startsWith(\"- [x] \");\n    const name = line.slice(\"- [?] \".length).trim();\n    return { name, isChecked };\n  });\n}\n\nasync function writeTodos(todos: Awaited\u003cReturnType\u003ctypeof readTodos\u003e\u003e) {\n  const content = todos\n    .map((todo) =\u003e `- [${todo.isChecked ? \"x\" : \" \"}] ${todo.name}`)\n    .join(\"\\n\");\n  await Bun.write(FILE_NAME, content);\n}\n```\n\nThis brought the correct behavior and stuff now worked!\nI was also able to ask the chat to mark a to-do as done and it would correctly\ncall the `toggle-todo` tool.\n\n- [ ] Add an option argument to `toggle-todo` to force direct on/off state\n\nI added another tool for deleting todos by a keyword similar to the one for\ntoggling them:\n\n```typescript\nserver.tool(\n  \"remove-todo\",\n  \"Removes a to-do item from the TODO.md file\",\n  {\n    keyword: z\n      .string()\n      .describe(\"A keyword in the name of the to-do item to remove\"),\n  },\n  async ({ keyword }) =\u003e {\n    const todos = await readTodos();\n    const index = todos.findIndex((todo) =\u003e todo.name.includes(keyword));\n    if (index === -1) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `To-do item containing \"${keyword}\" not found.`,\n          },\n        ],\n      };\n    }\n\n    const removedTodo = todos.splice(index, 1)[0];\n    await writeTodos(todos);\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Removed to-do item \"${removedTodo.name}\".`,\n        },\n      ],\n    };\n  }\n);\n```\n\nThis brings the basic behavior to completion.\nThere was many ways in which this toy MCP server could be improved which I will\nsave for the future.\n\n---\n\nUpon returning to this demo yet again, I have discovered the LLM can be smart\nenough to associate even short prompts with the MCP server tools at times.\n\nThese are the types of prompts that I have found work well to invoke the `to-do`\nMCP with GitHub Copilot chat in agent mode and using the OpenAI GPT-4.1 model.\n\nThese work without `.github/copilot-instructions.md`! (More on that below.)\n\n- \"Create a new to-do item to…\"\n- \"Make a new to-do for…\"\n- \"Remind me to…\"\n- \"Note to…\"\n\nI am positively surprised at the short ones!\nI was not able to reproduce the issue where it would bypass the MCP server and\nmake a `TODO.md` file itself from before even when resetting the chat history.\n\nOn the note of resetting the chat history, to get a sense of how a prompt would\nbe handled with no prior context:\n\nUse the Cmd+Shift+P command palette entry called Chat: Clear All Workspace Chats\nto reset the GitHub Copilot conversation to make sure prior context is not taken\ninto an account when putting in a new standalone prompt.\n\nI've also discovered the existence of the `.github/copilot-instructions.md` file\nwhich embeds extra repository-level context to every prompt / GitHub Copilot\nchat and can be used to steer the agent towards the desired behavior.\n\nI've used it to force the agent to always name the items in sentence case and\nlist the updated list after every mutation.\n\nUpdated to this file are picked up by the next prompt send to GitHub Copilot, it\njust needs to be saved, no need to refresh it it any other way.\n\n## HTTP Streamable Transport\n\nSo far this repository has implemented standard I/O transport.\nTo change it over to HTTP transport, the `StreamableHTTPServerTransport`\ntransport implementation can be used.\nThere's also `SSEServerTransport` when planning to use Server-Sent Events.\n\nI changed the code over, but a part is missing.\nA separate HTTP server needs to be run and `handleRequest` be used.\nIts signature is compatible only with Node server:\n\nhttps://nodejs.org/api/http.html#class-httpserver\n\n`Bun.serve` cannot be used as its `request` and `response` types are not\ncompatible:\n\nhttps://bun.sh/docs/api/http\n\nI let Claude Code implement the Node server and make a test for it in form of\nthe `test-server.sh` Bash file.\n\nI also tested it manually by updating `.github/mcp.json` to use the HTTP-based\ntransport and using VS Code GitHub Copilot agent to ask it about TODO-related\nstuff and verifying it kept using the right tools and updated `TODO.md`.\n\nI later found out the `StreamableHTTPServerTransport` defaults to SSE but this\nis different from `SSEServerTransport`.\nI wanted JSON transport to make the Bash script for testing the MCP exchange\neasier to test with `jq` so I turned on `enableJsonResponse` which switches from\nSSE to JSON.\n\nI asked Claude Code to research the differences between SSE in the streamable\nHTTP transport and the SSE transport and it concluded that `SSEServerTransport`\nis an old standard which was superseded by `StreamableHTTPServerTransport` which\nstill defaults to SSE, but can fall back to JSON mode, which is more useful for\nmy testing here.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTomasHubelbauer%2Fbun-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTomasHubelbauer%2Fbun-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTomasHubelbauer%2Fbun-mcp/lists"}