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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 0 additions & 48 deletions cliff.mcp.toml

This file was deleted.

82 changes: 10 additions & 72 deletions packages/loopover-mcp/bin/loopover-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -5192,10 +5133,11 @@ function toolsCommand(args: any) {
});
}

// `tools search <query>` — 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 <query>` — 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("--"));
Expand Down Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion packages/loopover-mcp/lib/local-branch.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
6 changes: 5 additions & 1 deletion packages/loopover-mcp/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down