ptc-fs-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ptc-fs-mcplist the files in the workspace root"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ptc-fs-mcp
A small filesystem MCP server: read and write files under one confined root, over stdio.
Demo software. It exists so that agentic runtimes have a real, deterministic external tool to point at in tutorials, examples, and integration tests. It is deliberately small enough to read in one sitting and copy into your own project. Do not deploy it as a production file service.
It was built for the PtcRunner agentic framework, where a filesystem capability arrives entirely through host configuration rather than runtime code. Nothing in the server is specific to PtcRunner — it speaks plain MCP over stdio, so any MCP client can install it.
npx -y ptc-fs-mcp --root ./workspace --include '**'Tools
Tool | Effect | Returns |
| read | Sorted, paginated entries under a relative prefix |
| read | Sorted, paginated paths containing a literal substring |
| read | Paginated literal matches with path and line evidence |
| read | Paginated exact UTF-8 byte chunks |
| write | Replaces one regular file, reports path and bytes |
The four read tools accept optional cursor and limit and return exactly
items, next_cursor, and content_hash. Start without a cursor and follow
next_cursor until it is null. For read_text_file, concatenating item text
reconstructs the file exactly.
Live bytes
Reads reflect the filesystem at call time, so a write is visible to the next read. That is the point of the server, and it has two consequences worth stating rather than discovering.
Cursors fail rather than tear. A cursor carries a digest of the state its
traversal depends on. If that state changed, the next page is rejected with
the filesystem changed since this cursor was issued; start the traversal again. A silently torn page — half from before the change, half from after —
is the one outcome worth spending an error on.
Only the state a result actually depends on is bound, so a cursor is not invalidated by an unrelated change:
Tool | Fails when | Survives |
| The listed entries change | A file appears deeper in a listed subdirectory |
| The matching path set changes | A matched file's contents are edited |
| Any in-scope file's contents or identity change | A change outside the searched prefix |
| That one file changes | Any other file changes |
Cursors are signed with a per-process key, bound to the tool and its arguments, and must be presented exactly as issued. A cursor from another traversal, another process, or an edited string is rejected.
Every result carries content_hash, the SHA-256 digest of the bytes that
call returned. A citation then names the bytes actually read rather than a tree
that happened to exist at some other moment. write_text_file reports the same
digest over the bytes it wrote, so a write and the read that follows it can be
checked against each other.
There is no whole-tree hash and no snapshot_identity to install. A digest can
only cover a bounded capture, and this server does not take one.
Running
ptc-fs-mcp --root ./workspace --include 'lib/**' --include 'docs/**' --exclude '**/secrets/**'Option | Meaning |
| Directory to confine to. Required. |
| Serve matching paths. Required, repeatable. |
| Never serve matching paths. Repeatable; may only narrow. |
| Do not serve files larger than this. |
| Largest |
--include is mandatory and the default is no files, so a server started
without it exposes nothing. Excluded paths are skipped before any stat or
open, so they are never inventoried. Globs match * within a segment and
** across segments; lib/** selects both lib/a.ts and lib/deep/a.ts.
Writes land in the root, so the include rules must reach it.
write_text_file names one basename, never a directory, so every write goes
directly into the root. An include set that only reaches into subdirectories —
--include 'lib/**' — serves those files for reading but can accept no write
at all, and each attempt is refused with no --include pattern of this root matches a file in the root itself. That is a legitimate configuration for a
read-only installation, so the server starts anyway and says so on stderr:
ptc-fs-mcp: no --include pattern matches a file in the root itself, so
write_text_file will refuse every call.Where the write tool is mapped, use --include '**' or add a root-level
pattern such as --include '*.md' alongside the directory ones.
Install it from a host document by pinning a version:
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "ptc-fs-mcp@0.1.0", "--root", "workspace", "--include", "**"],
"inherit_environment": true
}Spawning without an inherited environment
That form needs PATH twice over: npx is found on it, and the installed
binary begins with #!/usr/bin/env node, which resolves the interpreter on it
as well. A host that spawns with a scrubbed environment — PtcRunner's
inherit_environment: false, which its own end-to-end tests use — cannot start
the server at all, and the failure arrives as an acquisition error such as
provider_unavailable rather than as anything naming PATH. Version managers
make this sharper, not softer: an nvm interpreter lives at a path like
~/.nvm/versions/node/v20.19.0/bin/node and exists nowhere else.
The two configurations are mutually exclusive. To spawn hermetically, install
the package ahead of time and name the interpreter and the script absolutely,
bypassing both npx and the shebang:
npm install ptc-fs-mcp@0.1.0
node -p process.execPath
node -p "require.resolve('ptc-fs-mcp/package.json').replace(/package\.json$/, 'dist/cli.js')""transport": {
"type": "stdio",
"command": "/absolute/path/to/bin/node",
"args": [
"/absolute/path/to/node_modules/ptc-fs-mcp/dist/cli.js",
"--root",
"/absolute/path/to/workspace",
"--include",
"**"
],
"inherit_environment": false,
"env": {}
}The server itself needs nothing from the environment: it spawns no process,
opens no network connection, and reads no variable of its own. --root is
resolved against the working directory, so make it absolute as well unless the
host sets a cwd you control. hermetic_workspace in
examples/ptc-host.json is this form.
Splitting authority without splitting servers
An MCP host chooses which upstream tools become capabilities, so one
installation of this package can map only read_text_file while a second
installation — pointed at a different root — maps only write_text_file. A
generated reader program then cannot resolve the write tool at all. See
examples/ptc-host.json.
Usage from Node
The package is also a library. openRoot validates the configuration and pins
the root; createServer builds the same McpServer the binary serves, and you
give it whatever transport you like.
import { createServer, openRoot } from 'ptc-fs-mcp'
const root = openRoot({ root: './workspace', include: ['**'], exclude: ['*.secret'] })
const server = createServer(root)
await server.connect(myTransport)examples/embed.mjs is a runnable version that writes a
file, reads it back, and searches it — all in one process over the SDK's
in-memory transport:
npm run build && node examples/embed.mjsopenRoot throws ConfigError on an unusable configuration, and tools raise
ToolError; both are exported, along with normalizeRelative, compileGlob,
createSelector, and DEFAULT_LIMITS, so a host can reuse the path contract
without reimplementing it. TypeScript declarations ship with the package.
Protocol
2026-07-28 only. There is no initialize fallback, no downgrade negotiation,
and no compatibility branch: a 2025-era opening is refused with the
unsupported-protocol-version error naming the profile this server implements.
Only the tools capability is advertised — no Roots, Sampling, Logging, or
Tasks.
Confinement
Relative paths only. Absolute paths,
./..segments, NUL bytes, and Windows separators are rejected rather than resolved.Symbolic links are skipped, never followed, so a link inside the root cannot reach bytes outside it. The final
openusesO_NOFOLLOW, so a link swapped in after the check still fails.A directory appears in a listing only because it holds something served, so an unserved directory's name never leaks.
write_text_fileaccepts one lowercase basename — no directories, no traversal — caps the payload, and confirms the destination is a regular file through the descriptor it will write rather than through a separatestata symlink could outrace. A destination outside--includeis refused, because a write you could not read back is a trap rather than a feature. Because a write lands in the root, include rules that reach only into subdirectories refuse every write; see Running.Path listings are content-blind; content tools refuse what they cannot decode.
read_text_filefails on a file that is not valid UTF-8, andsearch_textskips a line whose bytes do not decode, so a line is either reported whole or not at all.Results are fitted against the full decoded MCP result, and text search also has a scan-byte budget. An empty search page can therefore carry a progress cursor when a sparse file needs more scanning.
Errors are short actionable text — no stacktraces, no host paths.
Nothing is spawned, no network is used, and stdout carries protocol messages only; diagnostics go to stderr.
What it does not defend
The root must be trusted and quiescent enough that a privileged actor is not racing you. Portable Node path APIs cannot descriptor-confine every ancestor directory, so an actor able to swap a parent directory mid-call is out of scope. The server rejects observed symlinks and uses a no-follow final open; it does not claim to defend an actively hostile source root.
Cursor staleness is detected from size, mtime, ctime, and inode number. On a filesystem with coarse timestamp granularity, an in-place rewrite of exactly the same length within the same timestamp tick would not be detected. Every mainstream filesystem this runs on records nanosecond times, and ctime is not settable from userspace.
Development
npm install
npm run build # tsc to dist/, with declarations and source maps
npm test # builds, then runs the suite against the built binary
npm run verify # format check, typecheck, and testsThe suite drives the built dist/cli.js as a real child process over real
stdio, so what ships is what is tested. Roots are generated per test rather
than committed, because this server writes as well as reads.
License
MIT. See LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Artifact store for AI agents. Hosted OAuth at mcp.artifacta.io/mcp; local stdio via npm/PyPI.
Project management MCP for AI agents with safe task reads and writes.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/andreasronge/ptc-fs-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server