Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ideas

Unvetted. Everything from a brainstorm lands here first, numbered, so nothing is lost between thinking of it and deciding about it. Items graduate to proposals.md once they have a shape, and from there to the feature list in story.md once built.

Nothing here is committed to. Some of it is probably wrong.

Format

Each item keeps the thinking, not a summary of it. A one-line version of “maybe support a list of dirs to add to the priority list, and allow either front or back, and maybe aliasing to dodge conflicts” loses the two open questions inside it, which are the part worth keeping.

So: a heading, the original thought in enough detail to reconstruct the intent, and where it went. → proposal means it has a written design. → shipped means it is built. → answered means it was a question, and the answer is recorded here, because an answer that only exists in a conversation is lost.

Should this be issues instead?

Probably, eventually. A file is right while there is one author and no repository to file against: it diffs, it reviews, and it cannot rot in a tracker nobody opens. The moment a second person is involved, or an item needs discussion threaded against it, these become issues and this file becomes an index of links. The graduation path is the same either way.


Intake 2026-09-27

Extracted from one message, grouped, not prioritised.


Paths and identity

1. Git version floor. Does --path-format=absolute need a new git? → answered. It landed in git 2.31, March 2021. The stated policy is that anything released before 2026-01-01 is fair game, so it is now used directly, with a hand-absolutising fallback kept for older git. Cost of the fallback is one extra git rev-parse only when the first attempt fails.

2. Is --git-common-dir relative to cwd or to the repo root? → answered. To cwd. Verified: from pkgs/mcpx/internal it returns ../../../.git. This matters because joining with cwd is therefore correct, and joining with the repo root would have been wrong. It is also why --show-toplevel needs no treatment: it is already absolute.

3. Is there a realpath that does not involve cwd? → answered. realpath(1) exists as a shell command, and Go’s filepath.EvalSymlinks is the in-process equivalent. Neither needs cwd for an absolute path; both resolve a relative one against cwd. So the order is forced: absolutise first, resolve second. Doing it the other way silently resolves against the wrong directory.

4. Would --absolute-git-dir work instead? → answered: no, for either scope. It returns the git directory, and for a linked worktree that is the worktree’s private .git/worktrees/<name>, not the shared common dir. Using it for repo scope would make every worktree look like a separate repository — the exact bug the scope exists to avoid. And for worktree scope the wanted value is the working tree root, not a git dir at all, so --show-toplevel is already the right call.

5. Prefer realpath everywhere for comparison and search. → shipped. Every path that becomes a key goes through one canonical() helper: absolutise, then EvalSymlinks, falling back to Clean when the path cannot be resolved. /tmp and /private/tmp now key identically, as do symlinked checkouts.

6. Are there cases where the relative form should win? Display, plausibly — repo:../.. is unreadable but repo:/Users/.../nix is long. Never identity. Open.


7. Implement round-robin budgeting. → shipped as mcpx catalog.

8. Keep whole-namespace types too, or force the budget on everyone? → answered: keep both. They answer different questions. catalog is “what exists, within a budget”; types is “I have chosen this namespace, give me all of it”. Collapsing them would make the common case need a flag, and would ration an agent that already knows what it wants.

9. How are signatures ranked — is there similarity indexing? An agent? A library? → answered: none of those. Catalogue ranking is by cost, len(line)/4, ties broken by path. No embeddings, no model, no dependency. That is deliberately different from mcpx search, which is relevance-ranked by substring scoring. Two rankings because they answer two questions.

10. Could a query bias what fits? → shipped as --bias, which promotes matching signatures ahead of the pure cost order. This is the merge of the two ranking systems and it seems to work: at a budget too small for take_screenshot, --bias screenshot promotes it anyway.

11. ls does not search — is that a separate thing? → answered: yes. ls enumerates namespaces and counts, never tools. search ranks tools across namespaces. catalog sits between them: every namespace, some tools.

12. Better search: typo tolerance, concept similarity, tags. → proposal. Worth noting the ordering: tags are cheap and exact, typo tolerance is cheap and fuzzy, similarity is expensive and needs a model or an index.

13. Types improvements beyond single-tool output. → proposal.


Protocols beyond MCP

14. Import an OpenAPI service as a namespace. → proposal. Confirmed feasible: the codegen already consumes JSON Schema, which is what an OpenAPI operation carries.

15. Emit OpenAPI per namespace. The inverse, so non-agent consumers can use the same servers without speaking MCP. → proposal.

16. Expose mcpx itself as an MCP server and/or an OpenAPI endpoint, optionally secured, with or without the MCP type wrappers. → proposal. This is distinct from 15: it makes mcpx a server, not just a client.

17. A common adapter interface so MCP, OpenAPI and anything later plug in the same way. → proposal.

18. Steal from MCP alternatives and mcp++ efforts. → proposal.


MCP server mechanics

19. How are Chrome debugging ports handled? → answered: they are not, and do not need to be. chrome-devtools-mcp has no listen-port flag. With --isolated it launches its own Chrome with a temporary profile and an ephemeral debugging port of Chrome’s own choosing. This is why mcpx has no port machinery, and why the predecessor’s auto-port code was solving a problem this server does not have.

20. Can you attach to different Chrome processes? → answered: yes, via --browserUrl http://127.0.0.1:9222 or --wsEndpoint ws://.... That is the case where per-instance templating becomes necessary, because each instance needs a different URL.

21. Auto-increment or random ports as a general feature. → proposal, as argument templating plus an allocator. Generalises beyond Chrome to any server that does take a port.

22. MCP servers have their own config — how is that handled? → answered: argv and environment, and that is all there is. MCP defines no configuration channel, so a server’s options are its command-line flags and its environment. mcpx passes command, args, env and cwd per server. The one thing MCP does define that mcpx was ignoring is instructions from initialize — now surfaced.

23. --pageIdRouting defaults to true and is documented as “useful for concurrent agent sessions”. Worth understanding before assuming pooling is the only answer to concurrency. Open.

24. What other common MCP settings are we missing? → proposal. Known gaps: sampling, roots, progress notifications, cancellation, elicitation, OAuth.


Scopes and sessions

25. Config policy for scopes — allow-list, default, and whether a flag or a script may override. → proposal.

26. Named sessions, and naming one that started anonymous. → proposal. The subtle part: an anonymous key is torn down when its caller exits, so naming it must flip that bit.

27. Per-scope keepalive defaults. → proposal.

28. Change keepalive at runtime, from CLI or script. → proposal.

29. Reap on an HTTP status code. → proposal, but MCP’s own ping is the better first version: no configuration and works for every server.

30. Reap on a webhook. → proposal. Splits into outbound (easy, it is events with an HTTP sink) and inbound (needs authentication; mcpx session stop already covers anything local).

31. Tie an instance to a pid. → shipped as scope: pid, reaped on the reaper tick when the process is gone rather than waiting out an idle timer.


Config and script discovery

32. Extra directories on the priority list, with a choice of front or back: --override-paths and --fallback-paths taking lists of directories or files. → proposal. The open question inside it is whether “front or back” is two flags or one flag plus an ordering rule.

33. Aliasing a file to a different name to dodge conflicts. → proposal, and flagged there as probably the wrong shape — shadowing already resolves conflicts by precedence, and an alias adds a second mechanism for the same job.

34. Do config and scripts share a directory? → answered: yes. .config/mcpx/config.json and .config/mcpx/scripts/*.ts, with .mcpx/ as the lower-precedence alternative spelling. Both are searched upward from cwd, and a directory carrying both spellings warns once.


Logging and output

35-43. Every command takes a display format; everything expressible as JSON is available as JSON; a logging helper for scripts accepting either a plain string or structured values; plain strings wrapped into the same record shape; automatic enrichment with ambient context; formats json / json-pretty / logfmt / text / compact / bare; message templates preserved next to the interpolated message; a default closer to a classic log line.

→ consolidated into one proposal. Answer to “does the existing logger interpolate”: there is no logger yet. The daemon prints unstructured lines and scripts have no helper at all.


Logging, round two

69. How does a script return several times? → answered: emit(), a separate export, not a log level. Logs and results share one wire channel with a kind discriminator so they stay ordered, but they are different functions with different destinations: logs go to stderr rendering, results to stdout or the envelope.

70. Should the discriminator be _kind? → answered: it is not a user-facing attribute at all. kind lives at the top of the wire record; user attributes are nested under attrs. A script writing log.info("x", {kind: "y"}) sets an attribute, not the discriminator. The real collision was elsewhere – see “Reserved attribute names”.

71. Should there be log.output() / log.return() helpers? Deliberately not. Logging and returning are different acts, and putting them on one object invites log.return being read as “log that we returned”. emit is the verb.

72. Can JS attributes merge onto Go context? → proposal, “Log context and enrichment layering”. slog does have the seam (Handler.Handle takes a context) and mcpx does not use it yet.

73. Does a streamed result also produce a log line? → answered: no. Verified: emit({a:1}) produces one stdout line and no record.

74. How does variadic translate between JS and Go? → answered: it does not, and should not. JS has real variadics, so log.info(msg, ...rest) with runtime type inspection is idiomatic. Go’s slog uses alternating key/value pairs (slog.Info("msg", "k", v)) or typed slog.Attr. Forcing either into the other’s shape would make one side unidiomatic. They meet at the wire format, which is a message plus a map, and each side reaches that in its own way.

75. How expensive is the Error-based source capture? → answered, measured: new Error().stack costs 4.89 us against a 0.10 us baseline for the JSON serialisation alone – roughly fifty times the cost of the record it decorates. Parsing the frame adds only 0.3 us on top. So capture is opt-in, and the expensive part is building the stack, not reading it.

76. Does JS run for every log message even when filtered? → answered: yes, partly, and that is worth fixing. The script always serialises and writes; the level threshold is applied in Go. A debug call in a hot loop therefore costs a write even when nothing will print. → proposal.

77. Should source capture be configurable globally and per server? → shipped for global (logging.source in config, plus the flag and the environment variable), and per-server logLevel now resolves. Per-server source is not meaningful, because source capture describes the script, not the server it is talking to.

Harness integration and distribution

44-53. Plugins for opencode 1 and 2, Claude, Codex; the split between an instructions file, plugin-injected instructions, and a tool surface; a plugin suppressing a redundant instructions file by detecting a marker; an install command per harness; hash-matching so an update never clobbers a user’s edits; daemon-side config updates; a curl-able install document; a curl-able setup document written for an agent rather than a human; mcpx setup-prompt / install-prompt; platform variants for nixos, flakes, darwin, darwin+homebrew.

→ consolidated into one proposal. The genuinely novel piece is the agent-targeted setup document: the artefact is a prompt, not a script.


Packaging and service

54-59. NixOS module and service; Homebrew formula; a non-flake nix installer; the binary installing its own service; autostart at boot versus on first use; whether sudo is ever appropriate.

→ consolidated into one proposal, with a firm view on the last one: never sudo. The daemon is per-user and owns per-user MCP servers; a system unit would run them as the wrong user and share state across users.


Transport, scale and language

60. What does the daemon actually speak? → answered. HTTP over two listeners: a unix socket for the CLI, and 127.0.0.1 on an OS-assigned ephemeral port for script clients, because fetch() over a unix socket is not portable across deno, bun and node. Not stdio, not named pipes, not files.

61. Is the port fixed or random? What if it is taken? → answered. Ephemeral, chosen by the OS, so there is nothing to collide with. It is published in a daemon record file that the CLI reads.

62. Can multiple daemons run? → answered: yes, already. One per config fingerprint, each with its own socket. mcpx daemons lists them.

63. Accept a URI with or without host and port (example.com, example.com:8008, :42). → proposal. Parsing is trivial; authentication is the actual blocker, because loopback currently provides trust for free.

64. Load testing. → proposal. Untested beyond 12 concurrent runs.

65. Go or Rust? → proposal, with a recommendation to stay: the workload is process supervision and JSON, where the runtime is idle most of the time.


Naming and comparison

66. mcpx, xmcp, or something else? → proposal. Short version: mcpx is fine, renaming gets more expensive weekly, and the only real argument is that mcpsh states the actual pitch.

67. What does opencode v2 have that this does not? → answered, and tracked: OAuth and elicitation for remote servers, in-sandbox search() so discovery never enters the transcript, a persistent PTY daemon, a shared background service, and OpenAPI import. mcpx is ahead on shell access, pooling and scoping, real-runtime TypeScript, and per-config isolation.

68. Track parity deliberately. → proposal.


Intake 2026-09-30

From one message about adapters, plus two language/transport requests. Filed straight to issues rather than proposals, per the graduation path above: these needed a design argument written down, not a line kept.


Adapters without hand-writing JSON

78. Can a binary become a declaration automatically, without a model? The AI-assisted version is already sketched (#114 has the --help parse plan, #83 has it as step 5), but both park it behind a large design and neither owns it. The deterministic half is shippable on its own. → #295.

The thing worth keeping from the thinking: --help is the worst regular source, not the best, and leading with it is why this looks harder than it is. Shell completion scripts are generated data structures — fish’s complete -c tool -l flag -d 'desc' is nearly a declaration already, and zsh _arguments specs carry flag, type and description in a fixed grammar. Cobra, clap and argparse all emit them. Ladder: completions → a published spec (which is paths.apis, not an adapter at all) → man → --help → --bare.

And completions need two ways in, not one. Extracting from the binary (tool completion fish, __complete) is the convenient path and only works when the program ships a generator. Accepting a completion file is the one that matters: distributions already ship completions for programs that have no generator at all — /usr/share/bash-completion/completions/*, zsh site-functions, the Homebrew and Nix prefixes — and those are frequently packager-written for old tools whose --help is the worst in the first place. It also makes the parser testable from a corpus instead of from installed binaries, lets someone fix the source and re-import rather than hand-editing generated JSON, and covers binaries mcpx cannot run at all. So extraction is sugar over the file parser; build the parser first. Best version of the idea: import searches the system completion directories by name before it ever probes a binary.

Also settled here: a generated declaration records the binary’s version, so mcpx adapter check can say “written against jq 1.7, found 1.8” instead of drifting silently. That was #83’s open question.

79. Can I just say git is an MCP that takes the subcommand and the flags? Not ideal but fast. → #295, as --bare, opt-in and loud.

Worth keeping why it is allowed at all, because #83 Approach A rejects the shell-escape version and the distinction is the whole argument: a bare adapter is an argv hole, not a shell hole. The cost is that it has no schema beyond “array of strings”, so the agent gets nothing to reason about and mcpx doctor can only check the binary exists. Hence: required description, a visible marker in mcpx adapter list, and probably a settings gate so an organisation can forbid it centrally.

The sharp edge found while writing it up: --bare on a binary that has its own argv-level exec escape is equivalent to Approach A. git -c core.pager='sh -c ...' is a shell escape mcpx cannot see. The sandbox is only as good as the wrapped program’s own argv surface, and the docs have to name the common offenders.

80. Go can exec a program so it can’t shell escape, right? → answered: yes, and it already does. Spec.Call builds an argv and runs exec.CommandContext(cctx, s.Command, full...) (internal/adapter/adapter.go:237). No /bin/sh -c anywhere in internal/adapter, so no word-splitting, globbing, ;, $(), backticks, pipes or redirection. The guarantee is real; see 79 for the one place it leaks.

81. Is there an issue for exec’ing bash, and for Python first-class? → answered: both already exist, no new issue needed. Shell and shebangs are #106 — its own notes already say “nearly just allow any shebang” and --shell||--bash||--shell python3. Python 1:1 with TypeScript is #105, framed there as “a second codegen backend and a second runner, not a second product”. Both sit under #175.


Languages and reach

82. Roc. → #296.

The reason it is not just “another interpreter”: Roc’s platform/application split means an application is pure and a platform supplies every effect it may have. That is the exact shape of an mcpx script — pure logic whose only effects are tool calls — and no other language in the runtime list models it natively. The dream version is mcpx as a Roc platform, where the capability boundary becomes the type system rather than a Deno flag set.

Why it is staged behind a codegen backend anyway: a Roc host is ordinarily Rust/Zig/C and links against the compiled app, so a Go host means cgo and a separate toolchain. Plus Roc is pre-1.0 with no compatibility promise. Ship the boring backend, keep the platform as research.

83. Iroh. → #297.

Reach a daemon by identity rather than by address — no port forwarding, no public IP, no ssh key copied. ssh (#85) solves encryption and auth and solves routing only if you can already reach the host, which is exactly the laptop-behind-NAT case.

The honest finding, kept because it is the part that would otherwise be rediscovered: iroh is Rust and mcpx is pure Go, and tsnet (Tailscale, embeddable in a Go program, mature hole-punching, real ACLs) and go-libp2p deliver most of the same property with no cgo. Where iroh genuinely wins is iroh-blobs — BLAKE3, content-addressed, resumable — which is the same shape #187 already wants for the dependency cache. If that convergence is the goal, iroh earns its sidecar; otherwise it probably does not.


The gap both of those exposed

84. Nobody knows who is calling the daemon. → #298, and the framing in the first draft was wrong.

Not a bug — it is written down three times as a deliberate choice (internal/daemon/server.go:214, internal/daemon/origin.go:13, internal/api/openapi.go:40): unauthenticated, socket file permissions are the access control, and the TCP bind host is deliberately not defaulted because “that has to be somebody’s decision rather than a default.”

The first draft called it blocking — a precondition for ssh, p2p and browser surfaces. → answered: no. There is no blocker for adding security and no blocker for not having it. An unauthenticated local daemon is a complete answer and stays one. The issue was rewritten to say so, and the auth coupling was stripped back out of the iroh (#297) and WASM (#299) issues, which should not carry it.

The rule that came out of it, worth keeping: do not mix auth with other things in issues. Auth work goes in one line — #253 (the interface) with the pure-auth issues ordered behind it — and other issues reference it without depending on it.

85. The permissive-interface trick. → #300, and this is the part worth remembering.

When a spec demands an interface mcpx does not have, the move is to build the interface and make it say yes: accept a user and a password, and reply “good job, you’re authorized.” That satisfies every conformance row asserting the endpoint exists, is well-formed and accepts a valid credential — which is most of #253’s 61 rows — and it cannot break anyone, because a daemon that authorizes everyone is exactly as secure as a daemon with no interface at all.

The rows that require an invalid credential to be refused are the ones that change behaviour and can lock you out of your own daemon. Those get their own issue (#300), ordered behind #253, blocking nothing.

The one hazard, and it is real: a permissive interface must be visibly permissive. mcpx doctor and startup should say “authorisation: permissive” so nobody ships it believing otherwise.

86. WASM as a tool source. → #299.

Distinct from #84, which is mcpx compiled to WASM. This is mcpx running a .wasm as a sandboxed tool source — a module reaches nothing until the host hands it an import, which is the inverse of internal/runner/runner.go:43’s admission that only Deno has a permission model worth the name.

What makes it cheap where 73 and 74 are expensive: wazero is a pure-Go WASM runtime with no cgo, so nix build keeps cross-compiling for free and the release matrix does not grow. The open tension is that the component model — which is what would make the capability set a per-module declaration — may require wasmtime and therefore cgo, forfeiting the advantage. That is the one piece of research gating the rest.