From bf93ee43e5ac3ebbeb2c4cec288726bbd8d6529a Mon Sep 17 00:00:00 2001 From: "Nikolai V." Date: Sun, 20 Sep 2026 00:09:13 +0400 Subject: [PATCH 1/5] Add plugin trace-mcp --- plugins/nikolai-vysotskyi/trace-mcp/LICENSE | 21 ++++ plugins/nikolai-vysotskyi/trace-mcp/README.md | 36 +++++++ plugins/nikolai-vysotskyi/trace-mcp/mcp.json | 9 ++ .../nikolai-vysotskyi/trace-mcp/plugin.json | 22 +++++ .../skills/trace-mcp-codemod/SKILL.md | 96 +++++++++++++++++++ .../skills/trace-mcp-pre-commit/SKILL.md | 79 +++++++++++++++ .../skills/trace-mcp-refactoring/SKILL.md | 86 +++++++++++++++++ .../trace-mcp/skills/trace-mcp/SKILL.md | 88 +++++++++++++++++ 8 files changed, 437 insertions(+) create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/LICENSE create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/README.md create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/mcp.json create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/plugin.json create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-codemod/SKILL.md create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-pre-commit/SKILL.md create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp/SKILL.md diff --git a/plugins/nikolai-vysotskyi/trace-mcp/LICENSE b/plugins/nikolai-vysotskyi/trace-mcp/LICENSE new file mode 100644 index 00000000..7604c9ed --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Nikolai Vysotskyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/nikolai-vysotskyi/trace-mcp/README.md b/plugins/nikolai-vysotskyi/trace-mcp/README.md new file mode 100644 index 00000000..9da714dd --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/README.md @@ -0,0 +1,36 @@ +# trace-mcp + +Code intelligence for MiniMax Code: one MCP tool call returns callers, callees, and framework edges across the repo, so the agent stops reading files one by one to answer "what uses this". + +Upstream source: https://github.com/nikolai-vysotskyi/trace-mcp + +## Try it + +```text +Find every caller of `validateHostedPluginDirectory` in this repo and tell me what breaks if I change its signature. +``` + +Expected result: the agent calls the trace-mcp search and impact tools and answers with the call sites plus the blast radius, without opening each file. On an unindexed project the agent runs `trace init` once first, then answers the same way. + +## Requirements + +- Node.js 22 or newer on `PATH`. +- The `trace-mcp` executable on `PATH` (`npm install -g trace-mcp`). `mcp.json` starts it as a stdio server with no arguments. +- macOS, Linux, or Windows. +- No account, no paid service, no API key. + +## Data and network + +- The code index is built and kept on the user's machine. Source code never leaves it. +- At most one anonymous usage ping per day (version, OS, MCP client, aggregate counts; no code, no paths, no per-install identifier beyond a locally generated UUID). Turn it off with `TRACE_MCP_TELEMETRY=off`, or with `"telemetry": { "usage_ping": false }` in `~/.trace/.config.json`. +- No other network access. No credentials in the package. + +## Skills and MCP + +Skills (each directory matches its frontmatter `name`): `trace-mcp` (routing: call trace-mcp instead of reading files when exploring a codebase), `trace-mcp-refactoring` (risk assessment and cross-file renames), `trace-mcp-codemod` (bulk mechanical edits), `trace-mcp-pre-commit` (security, quality-gate, and antipattern checks before committing). + +MCP: one stdio server, `trace-mcp`. Upstream counts: 182 tools, 81 languages, 88 framework integrations. + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json new file mode 100644 index 00000000..5bf3f43a --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "trace-mcp": { + "type": "stdio", + "command": "trace-mcp" + } + } +} diff --git a/plugins/nikolai-vysotskyi/trace-mcp/plugin.json b/plugins/nikolai-vysotskyi/trace-mcp/plugin.json new file mode 100644 index 00000000..2ad02a81 --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/plugin.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "trace-mcp", + "version": "3.28.0", + "description": "Framework-aware code intelligence MCP server — 88 framework integrations, 81 languages, 72.7% fewer input tokens to review a pull request, comprehension at parity", + "author": { + "name": "Nikolai Vysotskyi", + "url": "https://github.com/nikolai-vysotskyi" + }, + "license": "MIT", + "repository": "https://github.com/nikolai-vysotskyi/trace-mcp", + "homepage": "https://trace-mcp.com", + "keywords": [ + "mcp", + "code-intelligence", + "static-analysis", + "refactoring", + "semantic-search", + "code-graph", + "framework-aware" + ] +} diff --git a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-codemod/SKILL.md b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-codemod/SKILL.md new file mode 100644 index 00000000..d3c47475 --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-codemod/SKILL.md @@ -0,0 +1,96 @@ +--- +name: trace-mcp-codemod +description: Use trace-mcp apply_codemod for any bulk mechanical change instead of repeated Edit calls. Activate whenever the same edit pattern would be applied 2+ times, across one file or many. +--- + +# trace-mcp — Codemod Workflow + +`apply_codemod` is the correct tool for any repeated mechanical change. Using `Edit` for the same pattern twice or more is a waste of tokens and is error-prone. + +## When to Use — HARD RULE + +If you are about to make the **same kind of change 2 or more times** — whether in one file or across many — stop and use `apply_codemod`. This includes: + +- Adding `async`/`await` to a set of functions +- Updating a function signature everywhere it is called +- Fixing import paths after a move +- Adding or removing keywords/decorators +- Wrapping calls in a logger, try/catch, or feature flag +- Replacing a deprecated API usage +- Any regex-replaceable refactor + +No exceptions. "It's just three edits" is still a violation — use `apply_codemod`. + +## Standard Workflow + +### 1. Preview with dry run (default) + +``` +apply_codemod({ + pattern: "oldFunction\\(", + replacement: "newFunction(", + file_pattern: "src/**/*.ts", + dry_run: true // default +}) +``` + +Review the preview: matched files, context lines, and replacement correctness. Look for false positives. + +### 2. Narrow scope when needed + +Use `filter_content` to only touch files that also contain a second marker: + +``` +apply_codemod({ + pattern: "extractNodes\\(", + replacement: "extractNodes(ctx, ", + file_pattern: "src/**/*.ts", + filter_content: "import.*extractNodes", + dry_run: true +}) +``` + +For patterns that cross line boundaries, enable multiline mode: + +``` +apply_codemod({ + pattern: "function\\s+foo\\([^)]*\\)\\s*\\{", + replacement: "async function foo() {", + multiline: true, + dry_run: true +}) +``` + +### 3. Apply the change + +``` +apply_codemod({ ..., dry_run: false }) +``` + +If more than 20 files are affected, add `confirm_large: true`. + +### 4. Reindex and verify + +- `register_edit` is not needed for codemods — `apply_codemod` handles reindexing internally. +- Run the test suite or `check_quality_gates` with `scope: "changed"`. + +## Planning Larger Changes + +For changes that span packages or require version awareness (e.g. upgrading a dependency), use `plan_batch_change` first: + +``` +plan_batch_change({ + package: "lodash", + from_version: "4.17.0", + to_version: "5.0.0" +}) +``` + +This returns an impact report with all affected files and import references. Combine it with `apply_codemod` for the actual rewrite. + +## Anti-Patterns to Avoid + +- Using `Edit` with `replace_all` for renames — use `apply_rename` (see `trace-mcp-refactoring`). +- Chaining 3–10 `Edit` calls with the same `old_string` pattern shape — use `apply_codemod`. +- Skipping the dry-run preview — always review matches first. +- Forgetting `confirm_large: true` on changes >20 files. diff --git a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-pre-commit/SKILL.md b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-pre-commit/SKILL.md new file mode 100644 index 00000000..f615b10a --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-pre-commit/SKILL.md @@ -0,0 +1,79 @@ +--- +name: trace-mcp-pre-commit +description: Run trace-mcp security, quality-gate, and antipattern checks before committing or opening a PR. Activate when the agent is about to create a commit or pull request in a project indexed by trace-mcp. +--- + +# trace-mcp — Pre-Commit & Pre-PR Checks + +Before creating a commit or opening a pull request, run the trace-mcp validation suite. Fix any critical or high findings before committing. + +## When to Use + +- The user asks to commit, stage, or push changes +- The user asks to open a PR +- The agent has finished implementing a feature or fix and is about to hand off + +## Checklist + +### 1. Security scan + +``` +scan_security({ rules: ["all"] }) +``` + +OWASP Top-10 vulnerability scan across the changed scope. If the change touches untrusted data flows, add: + +``` +taint_analysis({}) +``` + +Trace untrusted sources to sensitive sinks (SQL, shell, file system, HTTP). + +### 2. Quality gates on the changed scope + +``` +check_quality_gates({ scope: "changed" }) +``` + +Validates complexity, coverage, duplication, and any project-configured gates on only the files you changed. + +### 3. Antipattern scan + +``` +detect_antipatterns({}) +``` + +Flags N+1 queries, eager loading, inefficient iteration, and language-specific performance footguns. + +### 4. Symbol-level diff for the PR description + +``` +compare_branches({ branch: "current" }) +``` + +Produces a symbol-level diff (functions added/removed/modified, signatures changed, exports changed). Use this as the basis for an accurate PR description instead of a raw line diff. + +### 5. Bug prediction (optional, for risky changes) + +``` +predict_bugs({}) +get_risk_hotspots({}) +``` + +Flags files where the combination of high complexity and high churn makes regressions likely. If your change touches a hotspot, add extra tests. + +## Fix or Escalate + +- **Critical / High findings:** fix before committing. Do not suppress without discussion. +- **Medium findings:** fix if cheap, otherwise note in the PR description. +- **Low / Info findings:** note in the PR description. + +## After Commit + +If the commit is part of a larger series, consider: + +``` +get_changed_symbols({ since: "" }) +``` + +to generate an accurate changelog entry grounded in the symbol graph rather than commit messages. diff --git a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md new file mode 100644 index 00000000..650d5344 --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md @@ -0,0 +1,86 @@ +--- +name: trace-mcp-refactoring +description: Safe refactoring workflow using trace-mcp — assess risk, find candidates, check impact, and rename symbols across all files without missing import sites or cross-file references. +--- + +# trace-mcp — Refactoring Workflow + +Use this skill whenever you are about to rename, restructure, extract, or otherwise refactor code in a project indexed by trace-mcp. The goal is to never break cross-file references and never guess at what is affected. + +## When to Use + +- Renaming a class, function, method, variable, or file +- Extracting a function or method +- Restructuring a module or splitting a file +- Changing a function signature +- Any change that touches more than one call site + +## Refactoring Workflow + +### 1. Assess before touching anything + +``` +assess_change_risk({ file_path: "src/foo.ts" }) +# or +assess_change_risk({ symbol_id: "" }) +``` + +This returns the risk level of the target change based on churn, complexity, fan-in/fan-out, and test coverage. Use it to decide whether to proceed, add tests first, or split the change. + +### 2. Find what actually needs refactoring + +``` +get_refactor_candidates() +``` + +Do not guess. This surfaces high-complexity, high-churn, and anti-pattern-laden symbols that are the real refactor targets. + +### 3. Know what will break + +``` +get_change_impact({ symbol_id: "" }) +``` + +Returns the reverse-dependency graph: every file, symbol, and test that depends on the target. Review this list before editing. + +### 4. Quantify complexity + +``` +get_complexity_report({ file_path: "src/foo.ts" }) +``` + +Gives you a baseline so you can verify the refactor actually reduced complexity. + +## Renaming a Symbol — MANDATORY Flow + +**Never** rename with `Edit` and `replace_all`. It silently misses import sites, re-exports, type references, and cross-file usages. + +``` +# 1. Collision detection first +check_rename({ symbol_id: "", target_name: "newName" }) + +# 2. Apply rename across ALL files (definition + every reference) +apply_rename({ symbol_id: "", new_name: "newName" }) +``` + +`apply_rename` updates the definition, imports, re-exports, call sites, JSX usages, and tests in one atomic operation. + +## Extracting a Function + +``` +extract_function({ + file_path: "src/foo.ts", + start_line: 42, + end_line: 67, + new_name: "computeTotals" +}) +``` + +Let trace-mcp handle the variable capture analysis — manual extraction routinely misses closure variables. + +## After the Refactor + +1. `register_edit` on each edited file to reindex +2. `get_complexity_report` again to confirm the reduction +3. `get_tests_for` the changed symbols — run them +4. `check_quality_gates` with `scope: "changed"` to verify no regressions diff --git a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp/SKILL.md b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp/SKILL.md new file mode 100644 index 00000000..9c1f5860 --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp/SKILL.md @@ -0,0 +1,88 @@ +--- +name: trace-mcp +description: Use trace-mcp tools for code navigation, impact analysis, and framework-aware queries instead of Read/Grep/Glob/Bash. Activate whenever the agent needs to explore, understand, or modify a codebase that has trace-mcp indexed. +--- + +# trace-mcp — Code Intelligence Routing + +trace-mcp is a framework-aware code intelligence MCP server. It exposes 182 tools that return semantic, structured results over a cross-language dependency graph. When trace-mcp is available, it is almost always cheaper and more accurate than native file tools. + +## When to Use + +Activate this skill whenever you need to: +- Find a function, class, method, route, component, or any symbol +- Understand a file, module, or feature before editing +- Determine what breaks if you change something +- Trace a request flow, call graph, or data flow +- Audit architecture, dead code, tests, or security + +**Do not use** `Read`, `Grep`, `Glob`, or shell `ls`/`find`/`cat`/`head`/`tail` for exploring source code (`.ts`, `.js`, `.py`, `.php`, `.go`, `.rb`, `.java`, etc.). Use trace-mcp tools instead. Native tools stay allowed only for non-code files (`.md`, `.json`, `.yaml`, configs) or immediately before an `Edit` on a known file. + +## Start-of-Session Checklist + +1. `get_project_map` with `summary_only=true` — orient yourself to the project structure +2. `get_task_context` with `task: ""` — gather all relevant code in a single call instead of chaining `search` → `get_symbol` → `Read` + +## Decision Matrix + +| Task | trace-mcp tool | Instead of | +|---|---|---| +| Find a symbol by name | `search` | Grep | +| Understand a file before editing | `get_outline` | Read (full file) | +| Read one symbol's source | `get_symbol` | Read (full file) | +| Multiple symbols + shared imports | `get_context_bundle` | chained `get_symbol` | +| What breaks if I change X | `get_change_impact` | guessing | +| Who calls this / what does it call | `get_call_graph` | Grep | +| All usages of a symbol | `find_usages` | Grep | +| Implementations of an interface | `get_implementations` | Grep / ls | +| Classes implementing X | `search` with `implements` filter | Grep | +| Tests for a symbol or file | `get_tests_for` | Glob + Grep | +| Project overview | `get_project_map` (summary_only) | Bash ls/find | +| Context for a task | `get_task_context` / `get_feature_context` | reading many files | +| HTTP request flow | `get_request_flow` | reading route + controller files | +| DB model relationships | `get_model_context` | reading model + migrations | +| Component tree | `get_component_tree` | reading component files | +| Circular dependencies | `get_circular_imports` | manual tracing | +| Dead code / dead exports | `get_dead_code` (`mode: "exports_only"`) | Grep for unused | +| Project health / coverage gaps | `self_audit` | manual inspection | +| Complexity / hotspots | `get_complexity_report` / `get_risk_hotspots` | guessing | + +## Token-Efficiency Rules + +1. **Batch independent queries.** Use `batch` when you need 2+ independent tool calls: + ``` + batch({ calls: [ + { tool: "get_outline", args: { path: "src/foo.ts" } }, + { tool: "get_outline", args: { path: "src/bar.ts" } }, + { tool: "search", args: { query: "handleRequest", kind: "function" } } + ]}) + ``` +2. **Never read the same file twice.** Use `get_outline` once, then `get_symbol` for specific pieces. +3. **Prefer `get_context_bundle`** over chained `get_symbol` calls — it deduplicates shared imports. +4. **Read-before-Edit optimization.** When you must `Read` a file to edit it: + - Call `get_outline` first to find the line range of the target symbol. + - Read only that range with `offset` + `limit`. Never read a 500-line file to edit 5 lines. +5. **Do not delegate code exploration to subagents.** Agent subprocesses carry ~50k tokens of overhead before doing anything. Use trace-mcp tools in the main conversation instead. + +## After Editing a File + +- Call `register_edit` with the edited `file_path` to reindex just that file and invalidate caches. This is much lighter than a full `reindex` and keeps subsequent queries accurate. +- If the response includes `_duplication_warnings`, review the referenced symbols — you may be duplicating existing logic. +- Do **not** re-read the file to "verify" the edit. The `Edit` tool already confirmed success. + +## Before Creating New Symbols + +- Call `check_duplication` with `{ name, kind }` to verify no similar symbol exists. Prevents reinventing existing logic. + +## Health Checks (Once Per Session) + +- `audit_config` — stale references in CLAUDE.md / settings +- `self_audit` — dead exports, untested code, hotspots +- `get_tech_debt` — per-module tech-debt grades +- `get_optimization_report` — detects repeated reads, Bash grep usage, missed trace-mcp opportunities + +## Related Skills + +- `trace-mcp-refactoring` — safe refactoring workflow (risk assessment → rename → impact check) +- `trace-mcp-codemod` — bulk mechanical changes via `apply_codemod` +- `trace-mcp-pre-commit` — security, quality-gate, and antipattern checks before commit From 89ae9733713627bdbc6cde6cc8dc5d46577bd6c7 Mon Sep 17 00:00:00 2001 From: "Nikolai V." Date: Mon, 21 Sep 2026 14:09:58 +0400 Subject: [PATCH 2/5] trace-mcp plugin: pin version, full network disclosure, writes section Addresses review points 1-3 on PR #54: version-pinned install with PATH caveat and provenance check, GA4 endpoint/fields/opt-outs with default-config scope, bulk-write disclosure for apply_rename/apply_codemod. --- plugins/nikolai-vysotskyi/trace-mcp/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/nikolai-vysotskyi/trace-mcp/README.md b/plugins/nikolai-vysotskyi/trace-mcp/README.md index 9da714dd..aa83522c 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/README.md +++ b/plugins/nikolai-vysotskyi/trace-mcp/README.md @@ -15,15 +15,20 @@ Expected result: the agent calls the trace-mcp search and impact tools and answe ## Requirements - Node.js 22 or newer on `PATH`. -- The `trace-mcp` executable on `PATH` (`npm install -g trace-mcp`). `mcp.json` starts it as a stdio server with no arguments. +- The `trace-mcp` executable on `PATH`, version 3.28.0 or newer (`npm install -g trace-mcp@3.28.0` — this package was validated against 3.28.0). `mcp.json` starts it as a stdio server with no arguments. Note the host cannot verify which binary answers to a bare `trace-mcp` name: a stale global install or any other same-named `PATH` entry wins silently, so keep the pinned version installed and, if your setup allows it, check the install with `npm audit signatures` (every trace-mcp release ships with Sigstore provenance). - macOS, Linux, or Windows. - No account, no paid service, no API key. -## Data and network +## Data and network (default configuration) -- The code index is built and kept on the user's machine. Source code never leaves it. -- At most one anonymous usage ping per day (version, OS, MCP client, aggregate counts; no code, no paths, no per-install identifier beyond a locally generated UUID). Turn it off with `TRACE_MCP_TELEMETRY=off`, or with `"telemetry": { "usage_ping": false }` in `~/.trace/.config.json`. -- No other network access. No credentials in the package. +- The code index is built and kept on the user's machine. With default settings, source code never leaves it. +- Two opt-in features change that, and neither is enabled by default: cloud embedding providers sit behind an explicit consent gate (`~/.trace/consent.json`, granted per provider) and send code excerpts to the configured provider; OTLP/Langfuse export sends spans to a backend you configure. Enabling either is a deliberate step outside this package's defaults. +- At most one anonymous usage ping per day, sent to Google's GA4 Measurement Protocol endpoint: a persistent locally-generated UUID (stored in `~/.trace/telemetry-state.json`), the trace-mcp version and previous version, install/upgrade signal, Node major, OS platform, timezone country, MCP client name and the model it mostly drove, number of indexed repositories, machine class (arch, cores, RAM in whole GB, kernel version), tool preset and advertised tool count, aggregate tool-call/saved-token deltas, and daemon start/crash counters. No code, no paths, no file names, no query content, no IP. Full field list: https://trace-mcp.com/privacy.html. Turn it off with `TRACE_MCP_TELEMETRY=off`, or with `"telemetry": { "usage_ping": false }` in `~/.trace/.config.json`. Suppressed automatically in CI. +- No other network access with default settings. No credentials in the package. + +## Writes + +Two of the server's tools modify the user's local checkout, nothing else: `apply_rename` rewrites a definition plus every reference in one operation, and `apply_codemod` applies pattern rewrites with a dry-run preview as the default — applying requires `dry_run: false`, and changes touching more than 20 files additionally require `confirm_large: true`. ## Skills and MCP From 810585b1f64610f7580b413dcaf114d5abb3f544 Mon Sep 17 00:00:00 2001 From: Nikolai Vysotskyi Date: Tue, 22 Sep 2026 08:12:24 +0400 Subject: [PATCH 3/5] Answer review round 2: npx-pinned launcher, full mutation surface, skill param fix - mcp.json runs npx -y trace-mcp@3.28.0 instead of a bare PATH binary, so the pinned registry version answers every launch and a stale global install or same-named PATH entry is never consulted. - README Writes section lists all six mutating tools with their exact gates (dry_run default, confirm_large past 20 files on rename and codemod only), states the two limits plainly (no automatic rollback, project-root confinement without symlink resolution), and notes the dev-preset requirement for the bundled skills. - Refactoring skill: extract_function example uses the schema's function_name (was new_name, which the tool rejects). --- plugins/nikolai-vysotskyi/trace-mcp/README.md | 17 +++++++++++++++-- plugins/nikolai-vysotskyi/trace-mcp/mcp.json | 3 ++- .../skills/trace-mcp-refactoring/SKILL.md | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/nikolai-vysotskyi/trace-mcp/README.md b/plugins/nikolai-vysotskyi/trace-mcp/README.md index aa83522c..33d8e600 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/README.md +++ b/plugins/nikolai-vysotskyi/trace-mcp/README.md @@ -15,7 +15,7 @@ Expected result: the agent calls the trace-mcp search and impact tools and answe ## Requirements - Node.js 22 or newer on `PATH`. -- The `trace-mcp` executable on `PATH`, version 3.28.0 or newer (`npm install -g trace-mcp@3.28.0` — this package was validated against 3.28.0). `mcp.json` starts it as a stdio server with no arguments. Note the host cannot verify which binary answers to a bare `trace-mcp` name: a stale global install or any other same-named `PATH` entry wins silently, so keep the pinned version installed and, if your setup allows it, check the install with `npm audit signatures` (every trace-mcp release ships with Sigstore provenance). +- `npx` on `PATH`. `mcp.json` starts the server as `npx -y trace-mcp@3.28.0` with no other arguments, so the exact validated version is resolved from the npm registry on every launch: a stale global install or any other same-named `PATH` entry is never consulted, and npm verifies the tarball integrity on download. Every trace-mcp release additionally ships with Sigstore provenance (`npm audit signatures`). Offline fallback: a pre-installed `trace-mcp` binary works if `trace-mcp --version` prints 3.28.0 or newer — minus the registry guarantee above, so prefer the `npx` form. - macOS, Linux, or Windows. - No account, no paid service, no API key. @@ -28,7 +28,20 @@ Expected result: the agent calls the trace-mcp search and impact tools and answe ## Writes -Two of the server's tools modify the user's local checkout, nothing else: `apply_rename` rewrites a definition plus every reference in one operation, and `apply_codemod` applies pattern rewrites with a dry-run preview as the default — applying requires `dry_run: false`, and changes touching more than 20 files additionally require `confirm_large: true`. +Six of the server's tools modify the user's local checkout; everything else is read-only. All six preview with `dry_run: true` by default and write nothing until re-called with `dry_run: false`: + +- `apply_rename` — renames a definition plus every reference in one operation, after collision detection. Past 20 files the apply fails closed (`success: false`, `Rename affects N files (>20). Pass confirm_large: true to proceed.`) and returns the preview so the call can be re-issued deliberately. +- `apply_codemod` — pattern rewrites (AST-aware on TypeScript/JavaScript, regex fallback elsewhere). Same >20-file `confirm_large` gate as rename, same fail-closed shape. +- `extract_function` — extracts a line range into a named helper (single file, TypeScript/JavaScript). Rejects multi-return slices with a structured error instead of guessing; lowers `confidence` on shadowed-variable cases. +- `apply_move` — moves a symbol between files, or renames/moves a file, updating imports. +- `change_signature` — adds, removes, renames, or reorders parameters and updates call sites. +- `remove_dead_code` — deletes one symbol after verifying it is dead (multi-signal detection or zero incoming edges), and warns about orphaned imports. + +`plan_refactoring` previews any rename/move/extract/signature change without touching files — the read-only way to review blast radius first. + +Two limits, stated plainly: applied edits are not rolled back automatically (a failed type-check after the fact is reported, not reverted — review the preview, or version-control the checkout), and every file argument is confined to the indexed project root (out-of-root paths are rejected before any write; no symlink resolution is claimed). + +The refactoring and codemod skills need these tools visible: under the server's default preset they are hidden (`Tool "apply_rename" is not available in this session's tool preset`), so load the `dev` preset first (`load_tools`). ## Skills and MCP diff --git a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json index 5bf3f43a..9427320f 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json +++ b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json @@ -3,7 +3,8 @@ "mcpServers": { "trace-mcp": { "type": "stdio", - "command": "trace-mcp" + "command": "npx", + "args": ["-y", "trace-mcp@3.28.0"] } } } diff --git a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md index 650d5344..72498e55 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md +++ b/plugins/nikolai-vysotskyi/trace-mcp/skills/trace-mcp-refactoring/SKILL.md @@ -72,7 +72,7 @@ extract_function({ file_path: "src/foo.ts", start_line: 42, end_line: 67, - new_name: "computeTotals" + function_name: "computeTotals" }) ``` From 41cac1848e0ee1348df0d300b63f2966d1b71a97 Mon Sep 17 00:00:00 2001 From: "Nikolai V." Date: Thu, 24 Sep 2026 20:08:16 +0400 Subject: [PATCH 4/5] Bump pin to trace-mcp@3.31.5 with write-boundary fix, re-verify --- plugins/nikolai-vysotskyi/trace-mcp/README.md | 4 ++-- plugins/nikolai-vysotskyi/trace-mcp/mcp.json | 2 +- plugins/nikolai-vysotskyi/trace-mcp/plugin.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/nikolai-vysotskyi/trace-mcp/README.md b/plugins/nikolai-vysotskyi/trace-mcp/README.md index 33d8e600..cb6bcab7 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/README.md +++ b/plugins/nikolai-vysotskyi/trace-mcp/README.md @@ -15,7 +15,7 @@ Expected result: the agent calls the trace-mcp search and impact tools and answe ## Requirements - Node.js 22 or newer on `PATH`. -- `npx` on `PATH`. `mcp.json` starts the server as `npx -y trace-mcp@3.28.0` with no other arguments, so the exact validated version is resolved from the npm registry on every launch: a stale global install or any other same-named `PATH` entry is never consulted, and npm verifies the tarball integrity on download. Every trace-mcp release additionally ships with Sigstore provenance (`npm audit signatures`). Offline fallback: a pre-installed `trace-mcp` binary works if `trace-mcp --version` prints 3.28.0 or newer — minus the registry guarantee above, so prefer the `npx` form. +- `npx` on `PATH`. `mcp.json` starts the server as `npx -y trace-mcp@3.31.5` with no other arguments, so the exact validated version is resolved from the npm registry on every launch: a stale global install or any other same-named `PATH` entry is never consulted, and npm verifies the tarball integrity on download. Every trace-mcp release additionally ships with Sigstore provenance (`npm audit signatures`). Offline fallback: a pre-installed `trace-mcp` binary works if `trace-mcp --version` prints 3.31.5 or newer — minus the registry guarantee above, so prefer the `npx` form. - macOS, Linux, or Windows. - No account, no paid service, no API key. @@ -39,7 +39,7 @@ Six of the server's tools modify the user's local checkout; everything else is r `plan_refactoring` previews any rename/move/extract/signature change without touching files — the read-only way to review blast radius first. -Two limits, stated plainly: applied edits are not rolled back automatically (a failed type-check after the fact is reported, not reverted — review the preview, or version-control the checkout), and every file argument is confined to the indexed project root (out-of-root paths are rejected before any write; no symlink resolution is claimed). +Two limits, stated plainly: applied edits are not rolled back automatically (a failed type-check after the fact is reported, not reverted — review the preview, or version-control the checkout), and every file argument is confined to the indexed project root (out-of-root paths are rejected before any write; writes addressed through symlinks pointing outside the root are refused at write time — verified against the pinned 3.31.5). The refactoring and codemod skills need these tools visible: under the server's default preset they are hidden (`Tool "apply_rename" is not available in this session's tool preset`), so load the `dev` preset first (`load_tools`). diff --git a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json index 9427320f..040fa9b6 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json +++ b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json @@ -4,7 +4,7 @@ "trace-mcp": { "type": "stdio", "command": "npx", - "args": ["-y", "trace-mcp@3.28.0"] + "args": ["-y", "trace-mcp@3.31.5"] } } } diff --git a/plugins/nikolai-vysotskyi/trace-mcp/plugin.json b/plugins/nikolai-vysotskyi/trace-mcp/plugin.json index 2ad02a81..79795035 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/plugin.json +++ b/plugins/nikolai-vysotskyi/trace-mcp/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "trace-mcp", - "version": "3.28.0", + "version": "3.31.5", "description": "Framework-aware code intelligence MCP server — 88 framework integrations, 81 languages, 72.7% fewer input tokens to review a pull request, comprehension at parity", "author": { "name": "Nikolai Vysotskyi", From bc461c77cef5facbdfc5a22de6aa64d5c11c2bf9 Mon Sep 17 00:00:00 2001 From: "Nikolai V." Date: Sat, 26 Sep 2026 04:27:41 +0400 Subject: [PATCH 5/5] Answer review round 3: daemonless runtime env, full write surface, exact-pin smoke mcp.json ships the shipped runtime's own controls as env: no daemon auto-spawn, no self-update, no usage ping, and the daemon-health poll redirected to a port this plugin never binds, so a foreign daemon (e.g. the desktop app's on 3741) is never adopted as the executing backend. README corrects the version claim to what initialize actually reports, documents daemon verification via /health, and discloses all write surfaces grouped by checkout edits, user tool-config, home-state stores, and in-band content. smoke/exact-pin-smoke.mjs reproduces the executable contract (14 assertions) with last-run.jsonl as the passing transcript. --- plugins/nikolai-vysotskyi/trace-mcp/README.md | 99 ++++- plugins/nikolai-vysotskyi/trace-mcp/mcp.json | 8 +- .../trace-mcp/smoke/exact-pin-smoke.mjs | 365 ++++++++++++++++++ .../trace-mcp/smoke/last-run.jsonl | 14 + 4 files changed, 476 insertions(+), 10 deletions(-) create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/smoke/exact-pin-smoke.mjs create mode 100644 plugins/nikolai-vysotskyi/trace-mcp/smoke/last-run.jsonl diff --git a/plugins/nikolai-vysotskyi/trace-mcp/README.md b/plugins/nikolai-vysotskyi/trace-mcp/README.md index cb6bcab7..756a31bf 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/README.md +++ b/plugins/nikolai-vysotskyi/trace-mcp/README.md @@ -15,20 +15,57 @@ Expected result: the agent calls the trace-mcp search and impact tools and answe ## Requirements - Node.js 22 or newer on `PATH`. -- `npx` on `PATH`. `mcp.json` starts the server as `npx -y trace-mcp@3.31.5` with no other arguments, so the exact validated version is resolved from the npm registry on every launch: a stale global install or any other same-named `PATH` entry is never consulted, and npm verifies the tarball integrity on download. Every trace-mcp release additionally ships with Sigstore provenance (`npm audit signatures`). Offline fallback: a pre-installed `trace-mcp` binary works if `trace-mcp --version` prints 3.31.5 or newer — minus the registry guarantee above, so prefer the `npx` form. +- `npx` on `PATH`. `mcp.json` starts the server as `npx -y trace-mcp@3.31.5` with no other arguments, so the exact validated version is resolved from the npm registry on every launch: a stale global install or any other same-named `PATH` entry is never consulted, and npm verifies the tarball integrity on download. Every trace-mcp release additionally ships with Sigstore provenance (`npm audit signatures`). The `env` block pins the runtime further: no daemon auto-spawn, no self-update, no usage ping (see Runtime below). Offline fallback: a pre-installed `trace-mcp` binary works if `trace-mcp --version` prints 3.31.5 or newer — minus the registry guarantee above, so prefer the `npx` form. - macOS, Linux, or Windows. - No account, no paid service, no API key. -## Data and network (default configuration) - -- The code index is built and kept on the user's machine. With default settings, source code never leaves it. +## Runtime: which code answers, and how to check + +`initialize` reports the version of the process that answers it — with the +shipped `env` block that is the pinned `trace-mcp@3.31.5` process, on every +launch. Three facts bound what that process may do behind the handshake: + +- Trace-mcp can execute calls on a background daemon (loopback HTTP, + default port 3741, e.g. the desktop app's) instead of in-process. Left at + defaults, a session proxies to a daemon it finds there — and + `TRACE_MCP_NO_DAEMON=1` alone only stops *spawning* one, not proxying to + one already running. This plugin therefore additionally points + `TRACE_MCP_DAEMON_PORT` at port 48171, which it never binds: the daemon + health poll cannot match, so no foreign backend is ever adopted, even when + the user runs a desktop daemon on 3741. Verified live: with a newer daemon + on 3741, the pinned session still answered `3.31.5` (see Smoke below). +- `TRACE_MCP_NO_AUTO_UPDATE=1` disables the self-update that would + otherwise replace the running install mid-session. +- `TRACE_MCP_TELEMETRY=off` disables the daily usage ping for sessions + launched through this plugin (remove the line to restore the documented + default ping described under Data and network). + +To audit a running setup independently: `initialize` gives the launcher +version, and `curl http://127.0.0.1:3741/health` reports a reachable +daemon's own version and pid (`{"status":"ok","version":"…","pid":…}`). +If both report the pinned version, launcher and backend coincide. + +## Data and network (as shipped by this plugin) + +- As launched by this plugin's `mcp.json`, the server makes no network + calls at all: the usage ping is forced off (`TRACE_MCP_TELEMETRY=off`) + and the self-update check is disabled, so there is nothing to phone home + to. The code index is built and kept on the user's machine; source code + never leaves it. - Two opt-in features change that, and neither is enabled by default: cloud embedding providers sit behind an explicit consent gate (`~/.trace/consent.json`, granted per provider) and send code excerpts to the configured provider; OTLP/Langfuse export sends spans to a backend you configure. Enabling either is a deliberate step outside this package's defaults. -- At most one anonymous usage ping per day, sent to Google's GA4 Measurement Protocol endpoint: a persistent locally-generated UUID (stored in `~/.trace/telemetry-state.json`), the trace-mcp version and previous version, install/upgrade signal, Node major, OS platform, timezone country, MCP client name and the model it mostly drove, number of indexed repositories, machine class (arch, cores, RAM in whole GB, kernel version), tool preset and advertised tool count, aggregate tool-call/saved-token deltas, and daemon start/crash counters. No code, no paths, no file names, no query content, no IP. Full field list: https://trace-mcp.com/privacy.html. Turn it off with `TRACE_MCP_TELEMETRY=off`, or with `"telemetry": { "usage_ping": false }` in `~/.trace/.config.json`. Suppressed automatically in CI. +- Without the plugin's `env` overrides (plain `trace-mcp` defaults), up to one anonymous usage ping per day goes to Google's GA4 Measurement Protocol endpoint: a persistent locally-generated UUID (stored in `~/.trace/telemetry-state.json`), the trace-mcp version and previous version, install/upgrade signal, Node major, OS platform, timezone country, MCP client name and the model it mostly drove, number of indexed repositories, machine class (arch, cores, RAM in whole GB, kernel version), tool preset and advertised tool count, aggregate tool-call/saved-token deltas, and daemon start/crash counters. No code, no paths, no file names, no query content, no IP. Full field list: https://trace-mcp.com/privacy.html. Turn it off with `TRACE_MCP_TELEMETRY=off`, or with `"telemetry": { "usage_ping": false }` in `~/.trace/.config.json`. Suppressed automatically in CI. - No other network access with default settings. No credentials in the package. ## Writes -Six of the server's tools modify the user's local checkout; everything else is read-only. All six preview with `dry_run: true` by default and write nothing until re-called with `dry_run: false`: +Write surfaces fall into four groups. Nothing below writes without being +called; read-only calls (`search`, `get_outline`, `plan_refactoring`, …) +never touch disk beyond the local index they read. + +**1. Checkout edits — six tools, all `dry_run: true` by default.** The +server advertises them only under the `dev` preset (`load_tools`), never +under default, and each call previews first and writes nothing until +re-called with `dry_run: false`: - `apply_rename` — renames a definition plus every reference in one operation, after collision detection. Past 20 files the apply fails closed (`success: false`, `Rename affects N files (>20). Pass confirm_large: true to proceed.`) and returns the preview so the call can be re-issued deliberately. - `apply_codemod` — pattern rewrites (AST-aware on TypeScript/JavaScript, regex fallback elsewhere). Same >20-file `confirm_large` gate as rename, same fail-closed shape. @@ -39,9 +76,53 @@ Six of the server's tools modify the user's local checkout; everything else is r `plan_refactoring` previews any rename/move/extract/signature change without touching files — the read-only way to review blast radius first. -Two limits, stated plainly: applied edits are not rolled back automatically (a failed type-check after the fact is reported, not reverted — review the preview, or version-control the checkout), and every file argument is confined to the indexed project root (out-of-root paths are rejected before any write; writes addressed through symlinks pointing outside the root are refused at write time — verified against the pinned 3.31.5). - -The refactoring and codemod skills need these tools visible: under the server's default preset they are hidden (`Tool "apply_rename" is not available in this session's tool preset`), so load the `dev` preset first (`load_tools`). +Two limits, stated plainly: applied edits are not rolled back automatically (a failed type-check after the fact is reported, not reverted — review the preview, or version-control the checkout), and every file argument is confined to the indexed project root (out-of-root paths are rejected before any write; writes addressed through symlinks pointing outside the root are refused at write time — both verified against the pinned 3.31.5, see Smoke). + +**2. Your own tool config — two tools.** `apply_startup_recommendations` +(`dry_run: true` by default) acts on the *user's* setup, not the indexed +checkout: it can disable an unused MCP server in the client config, move an +unused skill aside, or delete duplicated instruction lines. Every write +lands a restorable backup first, and `rollback_startup_recommendations` +undoes one apply byte-for-byte (latest backup by default). + +**3. Local state under `~/.trace/` — never the checkout.** Decision memory +(`remember_decision` and friends) persists to `decisions.db`; agent task +state (`trace_state_*`) to `state.db`; learned ranking weights +(`tune_weights`, itself `dry_run: true` by default and inert unless +telemetry is enabled) to `tuning.jsonc`; packed corpora +(`build_corpus`/`delete_corpus`) to `corpora/`; graph checkpoints +(`snapshot_graph`) to the snapshots store; the startup-recommendation +backups above to their backup dir. Index maintenance (`reindex`, +`register_edit`, `embed_repo`, `subproject_sync`, …) rewrites only the +local index and is idempotent — re-running converges to the same state. + +**4. Content returned in-band, not written.** `generate_docs`, +`generate_sbom`, and the various `export_*` tools return the generated +document as the call result; they create no files. (Their non-readonly +protocol annotations are conservative.) `visualize_graph` and +`visualize_subproject_topology` are the exception: they write one HTML +file and return its `outputPath`. + +The refactoring and codemod skills need the checkout tools visible: under the server's default preset they are hidden (`Tool "apply_rename" is not available in this session's tool preset`), so load the `dev` preset first (`load_tools`). + +## Smoke: reproducing the executable claims + +`smoke/exact-pin-smoke.mjs` (Node 22+, stdlib only) spawns the pinned +build over stdio with the same environment `mcp.json` ships, against a +scratch fixture with an outside-root canary, and asserts: the handshake +version equals the pin; the default surface holds 29 tools with no +mutators while `dev` holds 46 with `dry_run` defaulting to true; +a read call resolves the fixture and writes nothing; rename dry-run +previews and the >20-file apply fails closed; traversal and symlink +writes are refused with the canary intact; one real `extract_function` +applies (execution is local, not stubbed); and no telemetry state is +written. `smoke/last-run.jsonl` is the transcript of the latest passing +run (14/14, 2026-09-26, with a newer third-party daemon present on the +default port to prove the session does not adopt it): + +```sh +node smoke/exact-pin-smoke.mjs [--pin trace-mcp@3.31.5] +``` ## Skills and MCP diff --git a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json index 040fa9b6..55ec3eee 100644 --- a/plugins/nikolai-vysotskyi/trace-mcp/mcp.json +++ b/plugins/nikolai-vysotskyi/trace-mcp/mcp.json @@ -4,7 +4,13 @@ "trace-mcp": { "type": "stdio", "command": "npx", - "args": ["-y", "trace-mcp@3.31.5"] + "args": ["-y", "trace-mcp@3.31.5"], + "env": { + "TRACE_MCP_NO_DAEMON": "1", + "TRACE_MCP_NO_AUTO_UPDATE": "1", + "TRACE_MCP_TELEMETRY": "off", + "TRACE_MCP_DAEMON_PORT": "48171" + } } } } diff --git a/plugins/nikolai-vysotskyi/trace-mcp/smoke/exact-pin-smoke.mjs b/plugins/nikolai-vysotskyi/trace-mcp/smoke/exact-pin-smoke.mjs new file mode 100644 index 00000000..2fa25d6a --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/smoke/exact-pin-smoke.mjs @@ -0,0 +1,365 @@ +#!/usr/bin/env node +// Exact-pin MCP smoke for the trace-mcp MiniMax plugin. +// +// Spawns the pinned registry build over stdio with the same environment +// mcp.json ships, then asserts the executable contract a reviewer can +// re-run: handshake version, default/dev surfaces, a read call, dry-run +// and confirm_large gates, traversal/symlink refusal, telemetry opt-out, +// one real local mutation, and no reachable daemon behind the session. +// +// Usage: node smoke/exact-pin-smoke.mjs [--pin trace-mcp@3.31.5] +// Node 22+, stdlib only. Prints one JSON object per line (JSONL) to stdout; +// exits 0 only if every assertion holds, 1 otherwise. No network access +// beyond the npm install npx itself performs; the session under test makes +// none (telemetry forced off, daemon spawn disabled, update check disabled). +// +// Every assertion runs inside an isolated HOME and a scratch fixture, so an +// ambient desktop-app daemon or developer checkout on the reviewer's machine +// cannot change the outcome: TRACE_MCP_DAEMON_PORT points at a port this +// script never binds, so the session's /health poll cannot match a foreign +// daemon and every call executes in the pinned npx process. + +import { spawn } from 'node:child_process'; +import { + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const PIN = process.argv.includes('--pin') + ? process.argv[process.argv.indexOf('--pin') + 1] + : 'trace-mcp@3.31.5'; +const EXPECTED_VERSION = PIN.split('@')[1]; +const MUTATING = [ + 'apply_rename', + 'apply_codemod', + 'extract_function', + 'apply_move', + 'change_signature', + 'remove_dead_code', +]; +// A high port nothing in this repo ever binds. The session polls exactly this +// port for a daemon; with auto-spawn off it stays unanswered, which pins +// execution to the local backend even when the user runs a desktop daemon on +// the default 3741. Mirrors the "env" block in mcp.json. +const DEAD_PORT = '48171'; + +const lines = []; +const note = (test, ok, extra = {}) => { + lines.push({ test, ok, ...extra }); + if (!ok) process.exitCode = 1; +}; + +const work = mkdtempSync(join(tmpdir(), 'trace-mcp-smoke-')); +const home = join(work, 'home'); +const proj = join(work, 'proj'); +const outside = join(work, 'outside'); +mkdirSync(home, { recursive: true }); +mkdirSync(proj, { recursive: true }); +mkdirSync(outside, { recursive: true }); + +// Fixture: one shared definition referenced from 26 modules (a rename of the +// shared symbol touches 27 files, tripping the >20 confirm gate) plus a +// function body worth extracting. +const FILE_COUNT = 26; +writeFileSync(join(proj, 'shared.js'), `export function sharedTarget() { return 0; }\n`); +for (let i = 0; i < FILE_COUNT; i++) { + writeFileSync( + join(proj, `mod${i}.js`), + `import { sharedTarget } from './shared.js';\nexport const value${i} = sharedTarget();\n`, + ); +} +writeFileSync( + join(proj, 'main.js'), + `import { sharedTarget } from './shared.js';\nexport function entry() {\n const base = sharedTarget();\n const doubled = base * 2;\n return doubled;\n}\n`, +); +const CANARY = 'CANARY: must never change\n'; +writeFileSync(join(outside, 'secret.js'), `export function hidden() {\n return 1;\n}\n// ${CANARY}`); + +const snap = () => { + const files = []; + const walk = (dir) => { + for (const e of readdirSync(dir).sort()) { + const p = join(dir, e); + const st = lstatSync(p); + if (st.isDirectory()) walk(p); + else if (st.isFile()) files.push([p.slice(proj.length), readFileSync(p, 'utf8')]); + } + }; + walk(proj); + return JSON.stringify(files); +}; +const before = snap(); + +// Ambient daemon sighting (informational only — the dead-port env makes the +// session immune to it, and assertion 2 proves that). +const health = async (port) => { + try { + const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000) }); + if (!res.ok) return { reachable: false }; + const body = await res.json(); + return { reachable: true, version: body.version, pid: body.pid }; + } catch { + return { reachable: false }; + } +}; +const ambient3741 = await health(3741); +const deadPort = await health(Number(DEAD_PORT)); +note('pre.daemon_sighting', true, { port_3741: ambient3741, dead_port_48171: deadPort }); + +// One link-out symlink: created after the baseline snapshot on purpose, so +// the tree-unchanged assertions below stay meaningful while the refusal path +// still executes against a live symlink. +let child; +const startSession = () => + spawn('npx', ['-y', PIN], { + cwd: proj, + env: { + ...process.env, + HOME: home, + TRACE_MCP_DATA_DIR: join(home, '.trace'), + TRACE_MCP_NO_DAEMON: '1', + TRACE_MCP_NO_AUTO_UPDATE: '1', + TRACE_MCP_TELEMETRY: 'off', + TRACE_MCP_DAEMON_PORT: DEAD_PORT, + NO_COLOR: '1', + }, + stdio: ['pipe', 'pipe', 'pipe'], + }); + +child = startSession(); +let buf = ''; +const pending = new Map(); +let nextId = 1; +child.stdout.on('data', (d) => { + buf += d.toString(); + let idx; + while ((idx = buf.indexOf('\n')) >= 0) { + const line = buf.slice(0, idx).trim(); + buf = buf.slice(idx + 1); + if (!line) continue; + try { + const msg = JSON.parse(line); + if (msg.id !== undefined && pending.has(msg.id)) { + pending.get(msg.id)(msg); + pending.delete(msg.id); + } + } catch { + /* stderr interleaved or partial frame: ignore */ + } + } +}); +let stderrTail = ''; +child.stderr.on('data', (d) => { + stderrTail += d.toString().slice(-2000); +}); +const call = (method, params = {}) => + new Promise((resolve, reject) => { + const id = nextId++; + const timer = setTimeout(() => { + pending.delete(id); + reject(new Error(`timeout waiting for ${method}`)); + }, 90000); + pending.set(id, (msg) => { + clearTimeout(timer); + resolve(msg); + }); + child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + }); +const tool = async (name, args) => { + const res = await call('tools/call', { name, arguments: args }); + const text = res.result?.content?.[0]?.text ?? JSON.stringify(res); + try { + return JSON.parse(text); + } catch { + return { _raw: text }; + } +}; +child.stdin.write( + JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n', +); + +try { + // 1. Handshake: the pinned binary answers for itself. + const init = await call('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'exact-pin-smoke', version: '1' }, + }); + const serverVersion = init.result?.serverInfo?.version; + note('handshake.initialize', serverVersion === EXPECTED_VERSION, { + serverVersion, + expected: EXPECTED_VERSION, + }); + + // 1b. Wait until the scratch fixture is indexed; refactor probes below + // operate on the symbol store, so an early run would test nothing. + let indexedFiles = 0; + let healthKeys = []; + for (let i = 0; i < 90; i++) { + const h = await tool('get_index_health', {}); + healthKeys = Object.keys(h.stats ?? h ?? {}); + indexedFiles = + h.stats?.totalFiles ?? h.stats?.fileCount ?? h.stats?.files ?? h.totalFiles ?? 0; + if (indexedFiles >= FILE_COUNT + 1) break; + await new Promise((r) => setTimeout(r, 2000)); + } + note('index.ready', indexedFiles >= FILE_COUNT + 1, { indexedFiles, healthKeys }); + + // 2. Default surface: read-only, mutators hidden. + const list1 = await call('tools/list', {}); + const names1 = (list1.result?.tools ?? []).map((t) => t.name); + const leaked = MUTATING.filter((m) => names1.includes(m)); + note('surface.default_preset', leaked.length === 0 && names1.length > 0, { + tool_count: names1.length, + leaked, + }); + + // 3. Dev surface via load_tools: mutators appear, dry_run defaults true. + await tool('load_tools', { preset: 'dev' }); + const list2 = await call('tools/list', {}); + const tools2 = list2.result?.tools ?? []; + const names2 = tools2.map((t) => t.name); + const missing = MUTATING.filter((m) => !names2.includes(m)); + const dryDefaults = {}; + for (const t of tools2) { + if (MUTATING.includes(t.name)) dryDefaults[t.name] = t.inputSchema?.properties?.dry_run?.default; + } + note('surface.dev_preset', missing.length === 0, { + tool_count: names2.length, + missing, + dry_run_defaults: dryDefaults, + }); + const annotations = {}; + for (const t of tools2) { + if (MUTATING.includes(t.name)) annotations[t.name] = t.annotations ?? null; + } + note('surface.mutator_annotations', true, { annotations }); + + // 4. A read call works, returns the fixture, and writes nothing. + const search = await tool('search', { query: 'sharedTarget', limit: 5 }); + const searchHit = JSON.stringify(search).includes('sharedTarget'); + note('read.search', searchHit && snap() === before, { + hit: searchHit, + tree_unchanged: snap() === before, + }); + + // Resolve one symbol id for the rename probes. + const outline = await tool('get_outline', { path: 'shared.js' }); + const text = JSON.stringify(outline); + const idMatch = + text.match(/"symbolId"\s*:\s*"([^"]+)"/) || + text.match(/"symbol_id"\s*:\s*"([^"]+)"/) || + text.match(/"id"\s*:\s*"([^"]+)"/); + const symbolId = idMatch?.[1]; + note('read.get_outline', !!symbolId, { symbolId: symbolId ?? null }); + + // 5. Traversal outside the root is refused in both modes; canary intact. + const travDry = await tool('apply_codemod', { + pattern: 'CANARY', + replacement: 'CANARY', + file_pattern: '../outside/*.js', + }); + const travApply = await tool('apply_codemod', { + pattern: 'CANARY', + replacement: 'CANARY', + file_pattern: '../outside/*.js', + dry_run: false, + }); + const canaryOk = readFileSync(join(outside, 'secret.js'), 'utf8').includes(CANARY.trim()); + note('boundary.codemod_traversal', travDry.success === false && travApply.success === false && canaryOk && snap() === before, { + dry_success: travDry.success, + apply_success: travApply.success, + canary_unchanged: canaryOk, + tree_unchanged: snap() === before, + }); + + // 6. Writes through an in-root symlink pointing outside are refused. + // secret.js holds a real function body (lines 1-3), so only the symlink + // guard can refuse this call — not the range check. + symlinkSync(join(outside, 'secret.js'), join(proj, 'link-out.js')); + const symApply = await tool('extract_function', { + file_path: 'link-out.js', + start_line: 2, + end_line: 2, + function_name: 'exfil', + dry_run: false, + }); + const canaryOk2 = readFileSync(join(outside, 'secret.js'), 'utf8').includes(CANARY.trim()); + note('boundary.symlink_write', symApply.success === false && canaryOk2 && snap() === before, { + success: symApply.success, + error: (symApply.error ?? '').slice(0, 160), + canary_unchanged: canaryOk2, + tree_unchanged: snap() === before, + }); + rmSync(join(proj, 'link-out.js')); + + // 7. One real mutation applies locally (execution is not a stub). + // Line 4 computes one value used below: a clean single-return slice. + const real = await tool('extract_function', { + file_path: 'main.js', + start_line: 4, + end_line: 4, + function_name: 'computeDoubled', + dry_run: false, + }); + const mainChanged = readFileSync(join(proj, 'main.js'), 'utf8').includes('computeDoubled'); + note('mutation.extract_applies', real.success === true && mainChanged, { + success: real.success, + error: (real.error ?? '').slice(0, 200), + files_modified: real.files_modified, + }); + + // Re-baseline: the extract above legitimately rewrote main.js. The rename + // probes below assert against the post-extract tree. + const mid = snap(); + + // 8. Dry-run rename previews without touching the tree. + if (symbolId) { + const dry = await tool('apply_rename', { symbol_id: symbolId, new_name: 'renamedTarget' }); + note('gate.rename_dryrun', dry.success !== false && snap() === mid, { + success: dry.success, + }); + + // 9. >20-file apply without confirm_large fails closed, tree identical. + const big = await tool('apply_rename', { + symbol_id: symbolId, + new_name: 'renamedTarget', + dry_run: false, + }); + note('gate.rename_confirm_large', big.success === false && snap() === mid, { + success: big.success, + error: (big.error ?? '').slice(0, 200), + files_modified: big.files_modified, + }); + } else { + note('gate.rename_dryrun', false, { skipped: 'no symbol id from get_outline' }); + note('gate.rename_confirm_large', false, { skipped: 'no symbol id from get_outline' }); + } + + // 10. Telemetry opt-out: no ping state under the isolated home. + const teleState = join(home, '.trace', 'telemetry-state.json'); + const teleStateLegacy = join(home, '.trace-mcp', 'telemetry-state.json'); + note('telemetry.opt_out', !existsSync(teleState) && !existsSync(teleStateLegacy), { + state_written: existsSync(teleState) || existsSync(teleStateLegacy), + }); +} catch (err) { + note('smoke.harness_error', false, { error: String(err && err.message ? err.message : err) }); +} finally { + try { + child.kill('SIGKILL'); + } catch { + /* already gone */ + } +} + +for (const l of lines) console.log(JSON.stringify(l)); +rmSync(work, { recursive: true, force: true }); diff --git a/plugins/nikolai-vysotskyi/trace-mcp/smoke/last-run.jsonl b/plugins/nikolai-vysotskyi/trace-mcp/smoke/last-run.jsonl new file mode 100644 index 00000000..4809219a --- /dev/null +++ b/plugins/nikolai-vysotskyi/trace-mcp/smoke/last-run.jsonl @@ -0,0 +1,14 @@ +{"test":"pre.daemon_sighting","ok":true,"port_3741":{"reachable":true,"version":"3.32.0","pid":47417},"dead_port_48171":{"reachable":false}} +{"test":"handshake.initialize","ok":true,"serverVersion":"3.31.5","expected":"3.31.5"} +{"test":"index.ready","ok":true,"indexedFiles":28,"healthKeys":["totalFiles","totalSymbols","totalEdges","totalNodes","totalRoutes","totalResourceRoutes","totalTestFixtureRoutes","totalComponents","totalMigrations","partialFiles","errorFiles"]} +{"test":"surface.default_preset","ok":true,"tool_count":29,"leaked":[]} +{"test":"surface.dev_preset","ok":true,"tool_count":46,"missing":[],"dry_run_defaults":{"apply_rename":true,"remove_dead_code":true,"extract_function":true,"apply_codemod":true,"apply_move":true,"change_signature":true}} +{"test":"surface.mutator_annotations","ok":true,"annotations":{"apply_rename":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},"remove_dead_code":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"extract_function":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},"apply_codemod":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"apply_move":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},"change_signature":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}}} +{"test":"read.search","ok":true,"hit":true,"tree_unchanged":true} +{"test":"read.get_outline","ok":true,"symbolId":"shared.js::sharedTarget#function"} +{"test":"boundary.codemod_traversal","ok":true,"dry_success":false,"apply_success":false,"canary_unchanged":true,"tree_unchanged":true} +{"test":"boundary.symlink_write","ok":true,"success":false,"error":"Refusing to write through symlink: /private/tmp/multica-task-2825211594/trace-mcp-smoke-EE1Axy/proj/link-out.js","canary_unchanged":true,"tree_unchanged":true} +{"test":"mutation.extract_applies","ok":true,"success":true,"error":"","files_modified":["main.js"]} +{"test":"gate.rename_dryrun","ok":true,"success":true} +{"test":"gate.rename_confirm_large","ok":true,"success":false,"error":"Rename affects 28 files (>20). Pass confirm_large: true to proceed.","files_modified":["shared.js","main.js","mod0.js","mod1.js","mod10.js","mod11.js","mod12.js","mod13.js","mod14.js","mod15.js","mod16.js","mod17.js","mod18.js","mod19.js","mod2.js","mod20.js","mod21.js","mod22.js","mod23.js","mod24.js","mod25.js","mod3.js","mod4.js","mod5.js","mod6.js","mod7.js","mod8.js","mod9.js"]} +{"test":"telemetry.opt_out","ok":true,"state_written":false}