Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Version pinning, piece 1: pins only.** A manifest entry's `version:`, or an
overlay's `server_version: {<server>: <version>}`, holds an npx-launched server at one
exact version. pmcp writes the pin into the server's `args` and every platform's
`install` argv, keeping the package name, all or nothing. A pin is ignored with a
warning, and the entry is left unchanged, when it is not one exact version, the package
slot is not a plain registry spec, the launcher is not a bare `npx`, or the entry's own
env could redirect npm. Refusal warnings never include the pin value or an argv, and
name a server only if pmcp ships it. `gateway.update_server` does not move a pinned
server, and `pmcp update` reports it as `[PINNED]`. See
[Consiliency/pmcp#294](https://github.com/Consiliency/pmcp/issues/294); the plan merged
in [Consiliency/pmcp#317](https://github.com/Consiliency/pmcp/pull/317).
- **`pmcp guidance --feedback-submission on|off`, and the
`enable_feedback_submission` key it writes.** This is the only setting that lets
PMCP post a feedback issue to GitHub on your behalf, and it is **off by default**.
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,40 @@ crashing the gateway — the shipped manifest always still loads.
> run when provisioned — treat an overlay file with the same trust as your own
> `.mcp.json`. Policy still applies (denied servers stay denied).

### Pinning a client version

To hold an npx-launched server at one version, add to `~/.pmcp/manifest.yaml` (or an
approved project overlay):

```yaml
server_version:
firecrawl: "3.25.5"
```

or `version: "3.25.5"` on a whole `servers:` entry. pmcp writes the pin into the server's
args and every install argv, keeping the package name. It must be one exact version: a
range, a dist-tag such as `latest`, build metadata or a package spec is ignored with a
warning (a pin from an earlier source, if any, stands). `server_version: {name: null}`
sets nothing; to remove a pin, delete its line.

Only npx servers whose package slot is a plain registry name (`pkg`, `pkg@1.2.3`,
`pkg@tag`) can be pinned this way, and only if the entry's own env cannot redirect npm:
locale and terminal keys, npm's logging/timing settings and the keys pmcp ships for that
server are fine; anything else (such as `npm_config_registry` or `NODE_OPTIONS`) makes pmcp
ignore the pin. Only a bare `npx` launcher is pinned (not a path such as `./npx` or
`node_modules/.bin/npx`; on Windows, `npx.cmd`/`npx.exe` too). The launcher of every
`install` argv is judged by the rules of the host pmcp runs on, whatever platform key it sits
under, so a `windows:` argv using `npx.cmd` refuses the pin on Linux or macOS. For uvx, pip,
cargo or docker, or any other case, pin the version in the server's own `command`/`args` in
`.mcp.json` or `.pmcp.json`.

The pin fixes the npm spec; npm resolves it in your own environment (your `.npmrc`, npm's
cache, and a `node_modules` in the directory pmcp runs in; a `.npmrc` there can also set
the npm keys allowed next to a pin). `gateway.update_server` does not move a pinned server.
`pmcp update` shows it as `[PINNED]` with the version you wrote, except when the entry
also sets an npm setting such as `npm_config_loglevel`: then it prints `[FAILED] … Could not
determine a registry package`, and still moves nothing.

### Adding Custom Servers

For MCP servers not in the manifest, add them to `~/.mcp.json`:
Expand Down
21 changes: 16 additions & 5 deletions src/pmcp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,15 +1005,26 @@ async def _call(tool_name: str, arguments: dict[str, object]) -> dict[str, objec
return

for item in results:
ok = bool(item.get("ok"))
status = "OK" if ok else "FAILED"
server = item.get("server", "unknown")
message = item.get("message", "")
print(f"[{status}] {server}: {message}")
print(_format_update_result(item))
finally:
await client_manager.disconnect_all()


def _format_update_result(item: dict[str, object]) -> str:
"""One `pmcp update` line: main's ``[OK]``/``[FAILED]``, or ``[PINNED]``.

A server whose manifest pin pmcp materialised was deliberately not moved,
so it is ``[PINNED]`` with that validated exact version, not ``[FAILED]``
(Consiliency/pmcp#294 piece 1). Every other result prints as before.
"""
server = item.get("server", "unknown")
pinned = item.get("pinned_version")
if pinned:
return f"[PINNED] {server}: pinned at {pinned}"
status = "OK" if bool(item.get("ok")) else "FAILED"
return f"[{status}] {server}: {item.get('message', '')}"


def _get_gateway_url() -> str:
"""Return the PMCP gateway MCP endpoint URL."""
return os.environ.get(
Expand Down
Loading
Loading