From 4dcb0d808eb65f26493c32b20d268150ae5f9fc9 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:49:25 -0700 Subject: [PATCH 1/2] chore(mcp): delete orphaned changelog config and dead schema literals, lock the package against unused locals cliff.mcp.toml was referenced by nothing -- the MCP changelog is generated by scripts/generate-mcp-changelog.ts via cliff.toml, and check-changelog.ts --mcp still passes without it. It carried a stale commit_preprocessors rewrite rule that read as live config. Five hand-written output-schema literals (repoContext, preflight, decisionPack, localStatus, agentPlan) sat unreferenced since #291. Enabling noUnusedLocals / noUnusedParameters is what keeps the next one from accumulating; it immediately surfaced two more dead items, both removed: isValidationStatus, superseded by isValidationStatusLike with callers reaching normalizeValidationStatus directly, and an unused node:path join import in local-branch.ts. Comment fixes for counts that had already rotted: the registration wrapper said 37 call sites, and the tools-search example cited get_subnet_stake_quote, a tool that does not exist in this repo. Both now describe the surface without pinning a number that goes stale. Refs #9516 --- cliff.mcp.toml | 48 ------------- packages/loopover-mcp/bin/loopover-mcp.ts | 82 +++-------------------- packages/loopover-mcp/lib/local-branch.ts | 2 +- packages/loopover-mcp/tsconfig.json | 6 +- 4 files changed, 16 insertions(+), 122 deletions(-) delete mode 100644 cliff.mcp.toml diff --git a/cliff.mcp.toml b/cliff.mcp.toml deleted file mode 100644 index 792c4329f5..0000000000 --- a/cliff.mcp.toml +++ /dev/null @@ -1,48 +0,0 @@ -[changelog] -header = "# Changelog\n\n" -body = """ -{% if version %}\ -## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ -## Unreleased -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - -### {{ group | striptags | trim | upper_first }} -{% for commit in commits %} -- {{ commit.message | upper_first }} -{% endfor %} -{% endfor %}\n -""" -trim = true -footer = "" - -[git] -conventional_commits = true -filter_unconventional = true -split_commits = false -tag_pattern = "^mcp-v[0-9].*" -sort_commits = "oldest" - -commit_preprocessors = [ - { pattern = " \\(#[0-9]+\\)$", replace = "" }, - { pattern = "^fix\\(docs\\): use Gittensor screenshot in homepage hero$", replace = "fix(release): create GitHub releases for MCP tag publishes" }, -] - -commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix\\(changelog\\)", skip = true }, - { message = "^fix", group = "Fixes" }, - { message = "^test", group = "Tests" }, - { message = "^docs\\(changelog\\)", skip = true }, - { message = "^chore\\(release\\)", skip = true }, - { message = "^ci\\(release\\)", skip = true }, - { message = "^ci\\(changelog\\)", skip = true }, - { message = "^ci", group = "CI" }, - { message = "^build", group = "Build" }, - { message = "^docs", group = "Docs" }, - { message = "^refactor", group = "Refactors" }, - { message = "^chore\\(deps\\)", group = "Dependencies" }, - { message = "^chore", group = "Chores" }, - { message = "^revert", group = "Reverts" }, -] diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 51f35b5ed8..f661042705 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -1732,8 +1732,8 @@ export const server = new McpServer({ version: packageVersion, }); -// #4777: register a stdio tool under its loopover_ name. Thin wrapper kept so all 37 call sites -// stay uniform with the rest of this file's registration style. +// #4777: register a stdio tool under its loopover_ name. Every tool registers through this wrapper, +// so registration style stays uniform across the file. // Telemetry await/flush lives in wrapStdioToolHandler (lib/telemetry.ts, unit-tested) — #6238 / #8690. // Reads telemetryState() HERE on purpose: registerStdioTool's second parameter is the TOOL's config and // shadows the module-level `config`, so a read inside a nested function would silently see the wrong object. @@ -2954,68 +2954,9 @@ registerStdioTool( async (input: any) => toolResult("LoopOver base-agent public-safe PR packet.", await agentPreparePrPacket(await withClientWorkspaceRoots(input))), ); -// ── Output schemas for structured tool responses (#291) ────────────────────── - -const repoContextOutputSchema = { - type: "object", - properties: { - repoFullName: { type: "string" }, - lane: { type: "string" }, - primaryLanguage: { type: ["string", "null"] }, - openIssueCount: { type: "number" }, - openPrCount: { type: "number" }, - }, - additionalProperties: true, -}; - -const preflightOutputSchema = { - type: "object", - properties: { - status: { type: "string", enum: ["pass", "warn", "fail", "unknown"] }, - signals: { type: "array", items: { type: "object" } }, - summary: { type: "string" }, - }, - additionalProperties: true, -}; - -const decisionPackOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - decisions: { type: "array", items: { type: "object" } }, - cachedAt: { type: ["string", "null"] }, - }, - additionalProperties: true, -}; - -const localStatusOutputSchema = { - type: "object", - properties: { - apiUrl: { type: "string" }, - package: { type: "object", properties: { name: { type: "string" }, version: { type: "string" } }, additionalProperties: true }, - hasToken: { type: "boolean" }, - profile: { type: "object", additionalProperties: true }, - authLogin: { type: ["string", "null"] }, - sessionExpiresAt: { type: ["string", "null"] }, - sourceUploadDefault: { type: "boolean" }, - sourceUploadSupported: { type: "boolean" }, - git: { type: "object", additionalProperties: true }, - }, - additionalProperties: true, -}; - -const agentPlanOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - actions: { type: "array", items: { type: "object" } }, - topAction: { type: ["object", "null"] }, - }, - additionalProperties: true, -}; - -// Attach outputSchema to key tools via registerTool with zod output schemas. -// All other tools continue to return unschematized text+structured content. +// Only this tool declares an outputSchema today; every other tool returns text + unschematized +// structured content. #9518 finishes the job by registering all of them from @loopover/contract, +// where each tool's output schema lives beside its input schema and is enforced by validate:mcp. registerStdioTool( "loopover_local_status_structured", @@ -5192,10 +5133,11 @@ function toolsCommand(args: any) { }); } -// `tools search ` — fuzzy discovery across the ~150-tool combined surface (#6300). Matches the -// query against each registered tool's name AND description (not name-only), so "stake" surfaces -// get_subnet_stake_quote even though "stake" is only in its description. Reuses this CLI's existing -// levenshteinDistance for typo tolerance rather than pulling in a fuzzy-match dependency. +// `tools search ` — fuzzy discovery across the whole registered tool surface (#6300). Matches +// the query against each registered tool's name AND description (not name-only), so "duplicate" +// surfaces loopover_check_before_start even though "duplicate" is only in its description. Reuses +// this CLI's existing levenshteinDistance for typo tolerance rather than pulling in a fuzzy-match +// dependency. function toolsSearchCommand(args: any) { const options = parseOptions(args); const query = args.find((arg: any) => !arg.startsWith("--")); @@ -6427,10 +6369,6 @@ function optionalNumber(value: any) { return Number.isFinite(parsed) ? parsed : undefined; } -function isValidationStatus(value: any) { - return Boolean(normalizeValidationStatus(value)); -} - function normalizeValidationStatus(value: any) { const text = String(value ?? "").trim().toLowerCase().replace(/[-\s]+/g, "_"); if (["passed", "pass", "success", "ok", "exit_0", "0"].includes(text)) return "passed"; diff --git a/packages/loopover-mcp/lib/local-branch.ts b/packages/loopover-mcp/lib/local-branch.ts index c6f67f5a0a..d7945ba050 100644 --- a/packages/loopover-mcp/lib/local-branch.ts +++ b/packages/loopover-mcp/lib/local-branch.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { realpathSync } from "node:fs"; -import { isAbsolute, join, relative, resolve } from "node:path"; +import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { assertScenarioLocalBranchInputSafe } from "@loopover/engine"; import { isCodeFile, isTestPath as isTestFile } from "@loopover/engine/signals/test-evidence"; diff --git a/packages/loopover-mcp/tsconfig.json b/packages/loopover-mcp/tsconfig.json index 3795e72d26..9aa54d7746 100644 --- a/packages/loopover-mcp/tsconfig.json +++ b/packages/loopover-mcp/tsconfig.json @@ -25,7 +25,11 @@ // Without this, the inherited root value ("./.tsbuildinfo") resolves relative to the ROOT config's // location, not this one -- both packages would then read/write the exact same cache file at the // repo root and corrupt each other's incremental state. - "tsBuildInfoFile": "./.tsbuildinfo" + "tsBuildInfoFile": "./.tsbuildinfo", + // #9516: five hand-written output-schema literals sat here unreferenced because nothing rejected + // dead locals. tsc is the gate now, so the next one fails the build instead of accumulating. + "noUnusedLocals": true, + "noUnusedParameters": true }, // Every bin/lib runtime module is real TypeScript (#7291, phased across #7328/#7329/#7330): tsc owns // the dist/ .js emit, which is gitignored -- contributors and tests never touch it. The glob stays From 6018fcb6342364978f60ef38b3063c1cc2a69da4 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:12:03 -0700 Subject: [PATCH 2/2] ci: build the engine for mcp/miner-only PRs, which already run typecheck The "Build engine package" step gated on backend/engine/ui, while "Typecheck" gated on backend/engine/mcp/miner. An mcp-only or miner-only PR therefore ran typecheck against an unbuilt engine: test/** imports @loopover/engine directly, and its "types" resolve to packages/loopover-engine/dist/index.d.ts, which that step is what produces. The result was a wall of phantom "Cannot find module '@loopover/engine'" errors plus every implicit-any that cascades from them, none of which related to the PR's actual diff. Widens the build gate to match typecheck's, the same fix typecheck itself already received for the mirror-image gap. Any trigger that typechecks must also have built what typechecking reads. Refs #9516 --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b5775de5..0ee5b9dd7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -467,8 +467,16 @@ jobs: key: turbo-code-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} restore-keys: | turbo-code-${{ hashFiles('package-lock.json') }}- + # mcp/miner are in this gate because "Typecheck" below already runs for them, and typecheck's real + # surface reaches @loopover/engine -- test/** imports it directly, and its "types" resolve to + # packages/loopover-engine/dist/index.d.ts, which only exists once this step has run. An mcp-only or + # miner-only PR therefore used to run Typecheck against an unbuilt engine and fail with a wall of + # phantom "Cannot find module '@loopover/engine'" errors (plus every TS7006 implicit-any that + # cascades from them), with no relation to the actual diff. Deliberately the same widening the + # Typecheck step itself already received for the mirror-image gap; the two conditions must stay in + # sync, since any trigger that typechecks must also have built what typechecking reads. - name: Build engine package - if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.miner == 'true' }} run: npx turbo run build --filter=@loopover/engine # Mirrors "MCP package check"/"Miner package check" below: the published npm tarball is a # different surface than the workspace build above (files field, forbidden paths/content,