al-sem 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// src/mcp/server.ts
|
|
3
|
+
//
|
|
4
|
+
// MCP server for al-sem. Exposes progressive-disclosure tools (list_findings, get_finding,
|
|
5
|
+
// list_hotspots, get_routine_summary, explain_path, get_analysis_health) over stdio.
|
|
6
|
+
//
|
|
7
|
+
// All tools sit on top of the projection layer — they never return the full SemanticModel
|
|
8
|
+
// or raw evidencePath unless explicitly requested via the get_finding / explain_path tools.
|
|
9
|
+
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { explainPathTool } from "./tools/explain-path.ts";
|
|
14
|
+
import { getAnalysisHealthTool } from "./tools/get-analysis-health.ts";
|
|
15
|
+
import { getFindingTool } from "./tools/get-finding.ts";
|
|
16
|
+
import { getRoutineSummaryTool } from "./tools/get-routine-summary.ts";
|
|
17
|
+
import { listFindingsTool } from "./tools/list-findings.ts";
|
|
18
|
+
import { listHotspotsTool } from "./tools/list-hotspots.ts";
|
|
19
|
+
import { listRollupsTool } from "./tools/list-rollups.ts";
|
|
20
|
+
|
|
21
|
+
export interface McpTool {
|
|
22
|
+
definition: {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
inputSchema: { type: "object"; properties: Record<string, unknown>; required?: string[] };
|
|
26
|
+
};
|
|
27
|
+
handle(
|
|
28
|
+
args: Record<string, unknown>,
|
|
29
|
+
): Promise<{ content: Array<{ type: "text"; text: string }> }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build the server, register the given tools. Returns the un-connected Server instance. */
|
|
33
|
+
export function buildServer(tools: McpTool[]): Server {
|
|
34
|
+
const server = new Server({ name: "al-sem", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
35
|
+
|
|
36
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
37
|
+
tools: tools.map((t) => t.definition),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
41
|
+
const tool = tools.find((t) => t.definition.name === req.params.name);
|
|
42
|
+
if (tool === undefined) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text" as const, text: `unknown tool: ${req.params.name}` }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return tool.handle((req.params.arguments as Record<string, unknown>) ?? {});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return server;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Main entrypoint: build a server with registered tools, connect over stdio. */
|
|
55
|
+
async function main(): Promise<void> {
|
|
56
|
+
const server = buildServer([
|
|
57
|
+
listFindingsTool,
|
|
58
|
+
listRollupsTool,
|
|
59
|
+
getFindingTool,
|
|
60
|
+
listHotspotsTool,
|
|
61
|
+
getRoutineSummaryTool,
|
|
62
|
+
explainPathTool,
|
|
63
|
+
getAnalysisHealthTool,
|
|
64
|
+
]);
|
|
65
|
+
const transport = new StdioServerTransport();
|
|
66
|
+
await server.connect(transport);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Run only when invoked directly (not when imported in tests).
|
|
70
|
+
if (import.meta.main) {
|
|
71
|
+
void main();
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/mcp/session.ts
|
|
2
|
+
//
|
|
3
|
+
// Per-session analyzeWorkspace cache + pre-projected findings.
|
|
4
|
+
// One cache entry per (workspaceRoot, alpackagesDir) tuple, keyed by their concatenation.
|
|
5
|
+
// The server holds these for the lifetime of the MCP session; analyze is run lazily on
|
|
6
|
+
// the first request that touches a given workspace.
|
|
7
|
+
|
|
8
|
+
import { type AnalyzeWorkspaceResult, analyzeWorkspace } from "../index.ts";
|
|
9
|
+
import { type FindingSummary, projectFinding } from "../projection/finding-summary.ts";
|
|
10
|
+
|
|
11
|
+
export interface SessionEntry {
|
|
12
|
+
key: string;
|
|
13
|
+
workspaceRoot: string;
|
|
14
|
+
alpackagesDir?: string;
|
|
15
|
+
result: AnalyzeWorkspaceResult;
|
|
16
|
+
/** Pre-projected (compact) findings for fast list_findings / filter / group queries. */
|
|
17
|
+
findings: FindingSummary[];
|
|
18
|
+
/** Index by Finding.id for get_finding. */
|
|
19
|
+
findingsById: Map<string, FindingSummary>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sessions = new Map<string, SessionEntry>();
|
|
23
|
+
|
|
24
|
+
/** Look up a cached session entry or analyze + cache. Deterministic by construction. */
|
|
25
|
+
export async function getSession(
|
|
26
|
+
workspaceRoot: string,
|
|
27
|
+
alpackagesDir?: string,
|
|
28
|
+
): Promise<SessionEntry> {
|
|
29
|
+
const key = `${workspaceRoot}|${alpackagesDir ?? ""}`;
|
|
30
|
+
const cached = sessions.get(key);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const result = await analyzeWorkspace({ workspaceRoot, alpackagesDir, deterministic: true });
|
|
33
|
+
const findings = result.findings.map((f) => projectFinding(f, result.model));
|
|
34
|
+
const entry: SessionEntry = {
|
|
35
|
+
key,
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
alpackagesDir,
|
|
38
|
+
result,
|
|
39
|
+
findings,
|
|
40
|
+
findingsById: new Map(findings.map((f) => [f.id, f])),
|
|
41
|
+
};
|
|
42
|
+
sessions.set(key, entry);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Clear all cached sessions. Test-only helper. */
|
|
47
|
+
export function clearSessions(): void {
|
|
48
|
+
sessions.clear();
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/mcp/tools/explain-path.ts
|
|
2
|
+
|
|
3
|
+
import type { EvidenceStep } from "../../model/finding.ts";
|
|
4
|
+
import type { McpTool } from "../server.ts";
|
|
5
|
+
import { getSession } from "../session.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* explain_path — enrich a finding's evidencePath with object/routine names and file:line:col.
|
|
9
|
+
* Designed for agent "why was this flagged?" walkthroughs.
|
|
10
|
+
*/
|
|
11
|
+
export const explainPathTool: McpTool = {
|
|
12
|
+
definition: {
|
|
13
|
+
name: "explain_path",
|
|
14
|
+
description:
|
|
15
|
+
"For a finding id, return its evidencePath where every step is enriched with object/routine name and file:line:col. Use to walk an agent through 'why this was flagged'.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
workspaceRoot: { type: "string" },
|
|
20
|
+
alpackagesDir: { type: "string" },
|
|
21
|
+
findingId: { type: "string" },
|
|
22
|
+
},
|
|
23
|
+
required: ["workspaceRoot", "findingId"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async handle(args) {
|
|
27
|
+
const workspaceRoot = args.workspaceRoot;
|
|
28
|
+
const findingId = args.findingId;
|
|
29
|
+
if (typeof workspaceRoot !== "string") {
|
|
30
|
+
return { content: [{ type: "text", text: "explain_path: workspaceRoot is required" }] };
|
|
31
|
+
}
|
|
32
|
+
if (typeof findingId !== "string") {
|
|
33
|
+
return { content: [{ type: "text", text: "explain_path: findingId is required" }] };
|
|
34
|
+
}
|
|
35
|
+
const session = await getSession(
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
38
|
+
);
|
|
39
|
+
const finding = session.result.findings.find((f) => f.id === findingId);
|
|
40
|
+
if (finding === undefined) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `explain_path: no finding with id '${findingId}'` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const routinesById = new Map(session.result.model.routines.map((r) => [r.id, r]));
|
|
47
|
+
const objectsById = new Map(session.result.model.objects.map((o) => [o.id, o]));
|
|
48
|
+
|
|
49
|
+
const enrich = (step: EvidenceStep): Record<string, unknown> => {
|
|
50
|
+
const routine = routinesById.get(step.routineId);
|
|
51
|
+
const object = routine ? objectsById.get(routine.objectId) : undefined;
|
|
52
|
+
return {
|
|
53
|
+
routineId: step.routineId,
|
|
54
|
+
routineName: routine?.name,
|
|
55
|
+
objectId: routine?.objectId,
|
|
56
|
+
objectName: object?.name,
|
|
57
|
+
operationId: step.operationId,
|
|
58
|
+
callsiteId: step.callsiteId,
|
|
59
|
+
loopId: step.loopId,
|
|
60
|
+
file: step.sourceAnchor.sourceUnitId,
|
|
61
|
+
line: step.sourceAnchor.range.startLine + 1,
|
|
62
|
+
column: step.sourceAnchor.range.startColumn + 1,
|
|
63
|
+
note: step.note,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const payload = {
|
|
68
|
+
findingId: finding.id,
|
|
69
|
+
detector: finding.detector,
|
|
70
|
+
title: finding.title,
|
|
71
|
+
steps: finding.evidencePath.map(enrich),
|
|
72
|
+
};
|
|
73
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/mcp/tools/get-analysis-health.ts
|
|
2
|
+
|
|
3
|
+
import type { McpTool } from "../server.ts";
|
|
4
|
+
import { getSession } from "../session.ts";
|
|
5
|
+
|
|
6
|
+
export const getAnalysisHealthTool: McpTool = {
|
|
7
|
+
definition: {
|
|
8
|
+
name: "get_analysis_health",
|
|
9
|
+
description:
|
|
10
|
+
"Return coverage stats, diagnostic categories, and per-detector stats. Use to decide how trustworthy the analysis is before relying on findings.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
workspaceRoot: { type: "string" },
|
|
15
|
+
alpackagesDir: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
required: ["workspaceRoot"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async handle(args) {
|
|
21
|
+
const workspaceRoot = args.workspaceRoot;
|
|
22
|
+
if (typeof workspaceRoot !== "string") {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: "get_analysis_health: workspaceRoot is required" }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const session = await getSession(
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
30
|
+
);
|
|
31
|
+
const cov = session.result.model.coverage;
|
|
32
|
+
|
|
33
|
+
// diagnostic categories — bucket by severity + stage
|
|
34
|
+
const byStage: Record<string, number> = {};
|
|
35
|
+
const bySeverity: Record<string, number> = {};
|
|
36
|
+
for (const d of session.result.diagnostics) {
|
|
37
|
+
byStage[d.stage] = (byStage[d.stage] ?? 0) + 1;
|
|
38
|
+
bySeverity[d.severity] = (bySeverity[d.severity] ?? 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const health = {
|
|
42
|
+
coverage: {
|
|
43
|
+
sourceUnitsTotal: cov.sourceUnitsTotal,
|
|
44
|
+
sourceUnitsParsed: cov.sourceUnitsParsed,
|
|
45
|
+
routinesTotal: cov.routinesTotal,
|
|
46
|
+
routinesBodyAvailable: cov.routinesBodyAvailable,
|
|
47
|
+
routinesParseIncompleteCount: cov.routinesParseIncomplete.length,
|
|
48
|
+
opaqueApps: cov.opaqueApps,
|
|
49
|
+
unresolvedCallsiteCount: cov.unresolvedCallsites.length,
|
|
50
|
+
dynamicDispatchSiteCount: cov.dynamicDispatchSites.length,
|
|
51
|
+
},
|
|
52
|
+
diagnostics: {
|
|
53
|
+
total: session.result.diagnostics.length,
|
|
54
|
+
byStage,
|
|
55
|
+
bySeverity,
|
|
56
|
+
},
|
|
57
|
+
detectorStats: session.result.detectorStats,
|
|
58
|
+
findingsTotal: session.findings.length,
|
|
59
|
+
};
|
|
60
|
+
return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/mcp/tools/get-finding.ts
|
|
2
|
+
|
|
3
|
+
import type { McpTool } from "../server.ts";
|
|
4
|
+
import { getSession } from "../session.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* get_finding — return the full Finding for a single id, including evidencePath,
|
|
8
|
+
* primaryLocation, actionableAnchor, fingerprint, and provenance.
|
|
9
|
+
* Use list_findings first to discover ids.
|
|
10
|
+
*/
|
|
11
|
+
export const getFindingTool: McpTool = {
|
|
12
|
+
definition: {
|
|
13
|
+
name: "get_finding",
|
|
14
|
+
description:
|
|
15
|
+
"Return the full Finding for a single id, including evidencePath, primaryLocation, actionableAnchor, fingerprint, and provenance. Use list_findings first to discover ids.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
workspaceRoot: { type: "string", description: "Absolute path to the AL workspace root." },
|
|
20
|
+
alpackagesDir: { type: "string", description: "Optional explicit .alpackages path." },
|
|
21
|
+
findingId: { type: "string", description: "The Finding.id returned by list_findings." },
|
|
22
|
+
},
|
|
23
|
+
required: ["workspaceRoot", "findingId"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async handle(args) {
|
|
27
|
+
const workspaceRoot = args.workspaceRoot;
|
|
28
|
+
const findingId = args.findingId;
|
|
29
|
+
if (typeof workspaceRoot !== "string") {
|
|
30
|
+
return { content: [{ type: "text", text: "get_finding: workspaceRoot is required" }] };
|
|
31
|
+
}
|
|
32
|
+
if (typeof findingId !== "string") {
|
|
33
|
+
return { content: [{ type: "text", text: "get_finding: findingId is required" }] };
|
|
34
|
+
}
|
|
35
|
+
const session = await getSession(
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
38
|
+
);
|
|
39
|
+
const finding = session.result.findings.find((f) => f.id === findingId);
|
|
40
|
+
if (finding === undefined) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `get_finding: no finding with id '${findingId}'` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { content: [{ type: "text", text: JSON.stringify(finding, null, 2) }] };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/mcp/tools/get-routine-summary.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
mayCommit,
|
|
5
|
+
publishesEventsOf,
|
|
6
|
+
reachableCoverage,
|
|
7
|
+
touchesDbOf,
|
|
8
|
+
writesTablesOf,
|
|
9
|
+
} from "../../detectors/capability-query.ts";
|
|
10
|
+
import { roleOf } from "../../model/entities.ts";
|
|
11
|
+
import type { Routine } from "../../model/entities.ts";
|
|
12
|
+
import type { McpTool } from "../server.ts";
|
|
13
|
+
import { getSession } from "../session.ts";
|
|
14
|
+
|
|
15
|
+
function project(routine: Routine, objectName: string | undefined) {
|
|
16
|
+
return {
|
|
17
|
+
id: routine.id,
|
|
18
|
+
name: routine.name,
|
|
19
|
+
objectId: routine.objectId,
|
|
20
|
+
objectName,
|
|
21
|
+
analysisRole: roleOf(routine),
|
|
22
|
+
kind: routine.kind,
|
|
23
|
+
bodyAvailable: routine.bodyAvailable,
|
|
24
|
+
parseIncomplete: routine.parseIncomplete,
|
|
25
|
+
loops: {
|
|
26
|
+
count: routine.features.loops.length,
|
|
27
|
+
types: routine.features.loops.map((l) => l.type),
|
|
28
|
+
},
|
|
29
|
+
recordOperations: routine.features.recordOperations.map((op) => ({
|
|
30
|
+
op: op.op,
|
|
31
|
+
tableId: op.tableId,
|
|
32
|
+
inLoop: op.loopStack.length > 0,
|
|
33
|
+
loopDepth: op.loopStack.length,
|
|
34
|
+
tempState: op.tempState,
|
|
35
|
+
})),
|
|
36
|
+
callSites: routine.features.callSites.map((cs) => ({
|
|
37
|
+
calleeText: cs.calleeText,
|
|
38
|
+
inLoop: cs.loopStack.length > 0,
|
|
39
|
+
})),
|
|
40
|
+
summary: routine.summary
|
|
41
|
+
? {
|
|
42
|
+
touchesDb: touchesDbOf(routine.summary),
|
|
43
|
+
commits: mayCommit(routine.summary),
|
|
44
|
+
writesTables: writesTablesOf(routine.summary),
|
|
45
|
+
publishesEvents: publishesEventsOf(routine.summary),
|
|
46
|
+
coverage: reachableCoverage(routine.summary),
|
|
47
|
+
dbEffects: routine.summary.dbEffects,
|
|
48
|
+
parameterRoles: routine.summary.parameterRoles,
|
|
49
|
+
}
|
|
50
|
+
: undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const getRoutineSummaryTool: McpTool = {
|
|
55
|
+
definition: {
|
|
56
|
+
name: "get_routine_summary",
|
|
57
|
+
description:
|
|
58
|
+
"Project a single routine into a compact view: role, loops, recordOperations, callSites, and the L4 summary (touchesDb, commits, writesTables, publishesEvents, coverage, dbEffects, parameterRoles). Specify by routineId, by objectId+routineName, or by sourceLocation { file, line }.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
workspaceRoot: { type: "string" },
|
|
63
|
+
alpackagesDir: { type: "string" },
|
|
64
|
+
routineId: { type: "string" },
|
|
65
|
+
objectId: { type: "string" },
|
|
66
|
+
routineName: { type: "string" },
|
|
67
|
+
sourceLocation: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: { file: { type: "string" }, line: { type: "integer" } },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ["workspaceRoot"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
async handle(args) {
|
|
76
|
+
const workspaceRoot = args.workspaceRoot;
|
|
77
|
+
if (typeof workspaceRoot !== "string") {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: "get_routine_summary: workspaceRoot is required" }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const session = await getSession(
|
|
83
|
+
workspaceRoot,
|
|
84
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
85
|
+
);
|
|
86
|
+
const objectsById = new Map(session.result.model.objects.map((o) => [o.id, o]));
|
|
87
|
+
|
|
88
|
+
let routine: Routine | undefined;
|
|
89
|
+
if (typeof args.routineId === "string") {
|
|
90
|
+
routine = session.result.model.routines.find((r) => r.id === args.routineId);
|
|
91
|
+
} else if (typeof args.objectId === "string" && typeof args.routineName === "string") {
|
|
92
|
+
const objId = args.objectId;
|
|
93
|
+
const rname = args.routineName;
|
|
94
|
+
routine = session.result.model.routines.find((r) => r.objectId === objId && r.name === rname);
|
|
95
|
+
} else if (args.sourceLocation !== undefined && typeof args.sourceLocation === "object") {
|
|
96
|
+
const loc = args.sourceLocation as { file?: unknown; line?: unknown };
|
|
97
|
+
if (typeof loc.file === "string" && typeof loc.line === "number") {
|
|
98
|
+
const wantFile = loc.file;
|
|
99
|
+
const wantLine = loc.line - 1; // input is 1-based; SourceAnchor is 0-based
|
|
100
|
+
routine = session.result.model.routines.find(
|
|
101
|
+
(r) =>
|
|
102
|
+
r.sourceAnchor.sourceUnitId === wantFile &&
|
|
103
|
+
r.sourceAnchor.range.startLine <= wantLine &&
|
|
104
|
+
wantLine <= r.sourceAnchor.range.endLine,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: "get_routine_summary: specify routineId, objectId+routineName, or sourceLocation",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (routine === undefined) {
|
|
119
|
+
return { content: [{ type: "text", text: "get_routine_summary: routine not found" }] };
|
|
120
|
+
}
|
|
121
|
+
const obj = objectsById.get(routine.objectId);
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(project(routine, obj?.name), null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/mcp/tools/list-findings.ts
|
|
2
|
+
|
|
3
|
+
import { filterFindings } from "../../projection/finding-filters.ts";
|
|
4
|
+
import type { McpTool } from "../server.ts";
|
|
5
|
+
import { getSession } from "../session.ts";
|
|
6
|
+
import { validateMinSeverity } from "./validators.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* list_findings — paged, filtered list of compact FindingSummary[] for a workspace.
|
|
10
|
+
* Designed for agent progressive disclosure: agents call this first, then get_finding
|
|
11
|
+
* for evidence on a specific id.
|
|
12
|
+
*/
|
|
13
|
+
export const listFindingsTool: McpTool = {
|
|
14
|
+
definition: {
|
|
15
|
+
name: "list_findings",
|
|
16
|
+
description:
|
|
17
|
+
"List analysis findings for an AL workspace, filtered and paged. Returns compact FindingSummary[]. Call get_finding(findingId) for the evidence path of a specific finding.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
workspaceRoot: { type: "string", description: "Absolute path to the AL workspace root." },
|
|
22
|
+
alpackagesDir: { type: "string", description: "Optional explicit .alpackages path." },
|
|
23
|
+
minSeverity: {
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: ["critical", "high", "medium", "low", "info"],
|
|
26
|
+
description: "Drop findings below this severity.",
|
|
27
|
+
},
|
|
28
|
+
detectors: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: { type: "string" },
|
|
31
|
+
description: "Allow-list of detector ids.",
|
|
32
|
+
},
|
|
33
|
+
objectIds: { type: "array", items: { type: "string" } },
|
|
34
|
+
tableIds: { type: "array", items: { type: "string" } },
|
|
35
|
+
files: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
description: "Match by file path suffix.",
|
|
39
|
+
},
|
|
40
|
+
limit: { type: "integer", minimum: 1 },
|
|
41
|
+
},
|
|
42
|
+
required: ["workspaceRoot"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
async handle(args) {
|
|
46
|
+
const workspaceRoot = args.workspaceRoot;
|
|
47
|
+
if (typeof workspaceRoot !== "string") {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{ type: "text", text: "list_findings: workspaceRoot is required and must be a string" },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const sev = validateMinSeverity(args.minSeverity, "list_findings");
|
|
55
|
+
if (!sev.ok) return { content: [{ type: "text", text: sev.error }] };
|
|
56
|
+
const session = await getSession(
|
|
57
|
+
workspaceRoot,
|
|
58
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
59
|
+
);
|
|
60
|
+
const filtered = filterFindings(session.findings, {
|
|
61
|
+
minSeverity: sev.value,
|
|
62
|
+
detectors: Array.isArray(args.detectors) ? (args.detectors as string[]) : undefined,
|
|
63
|
+
objectIds: Array.isArray(args.objectIds) ? (args.objectIds as string[]) : undefined,
|
|
64
|
+
tableIds: Array.isArray(args.tableIds) ? (args.tableIds as string[]) : undefined,
|
|
65
|
+
files: Array.isArray(args.files) ? (args.files as string[]) : undefined,
|
|
66
|
+
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
findings: filtered,
|
|
75
|
+
total: filtered.length,
|
|
76
|
+
totalUnfiltered: session.findings.length,
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/mcp/tools/list-hotspots.ts
|
|
2
|
+
|
|
3
|
+
import { filterFindings } from "../../projection/finding-filters.ts";
|
|
4
|
+
import { type GroupBy, groupFindings } from "../../projection/finding-groups.ts";
|
|
5
|
+
import type { McpTool } from "../server.ts";
|
|
6
|
+
import { getSession } from "../session.ts";
|
|
7
|
+
import { validateMinSeverity } from "./validators.ts";
|
|
8
|
+
|
|
9
|
+
const VALID_GROUP_BY: ReadonlyArray<GroupBy> = ["object", "routine", "table", "detector", "file"];
|
|
10
|
+
|
|
11
|
+
export const listHotspotsTool: McpTool = {
|
|
12
|
+
definition: {
|
|
13
|
+
name: "list_hotspots",
|
|
14
|
+
description:
|
|
15
|
+
"Group findings (object / routine / table / detector / file) and return the top-N groups with counts. Use to surface the highest-leverage places to look first.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
workspaceRoot: { type: "string" },
|
|
20
|
+
alpackagesDir: { type: "string" },
|
|
21
|
+
groupBy: {
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: ["object", "routine", "table", "detector", "file"],
|
|
24
|
+
description: "Which dimension to group by.",
|
|
25
|
+
},
|
|
26
|
+
minSeverity: {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: ["critical", "high", "medium", "low", "info"],
|
|
29
|
+
description: "Drop findings below this severity before grouping.",
|
|
30
|
+
},
|
|
31
|
+
limit: { type: "integer", minimum: 1, description: "Top-N group cap." },
|
|
32
|
+
},
|
|
33
|
+
required: ["workspaceRoot", "groupBy"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async handle(args) {
|
|
37
|
+
const workspaceRoot = args.workspaceRoot;
|
|
38
|
+
const groupByArg = args.groupBy;
|
|
39
|
+
if (typeof workspaceRoot !== "string") {
|
|
40
|
+
return { content: [{ type: "text", text: "list_hotspots: workspaceRoot is required" }] };
|
|
41
|
+
}
|
|
42
|
+
if (typeof groupByArg !== "string" || !VALID_GROUP_BY.includes(groupByArg as GroupBy)) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: `list_hotspots: groupBy must be one of: ${VALID_GROUP_BY.join(", ")}`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const groupBy = groupByArg as GroupBy;
|
|
53
|
+
const sev = validateMinSeverity(args.minSeverity, "list_hotspots");
|
|
54
|
+
if (!sev.ok) return { content: [{ type: "text", text: sev.error }] };
|
|
55
|
+
|
|
56
|
+
const session = await getSession(
|
|
57
|
+
workspaceRoot,
|
|
58
|
+
typeof args.alpackagesDir === "string" ? args.alpackagesDir : undefined,
|
|
59
|
+
);
|
|
60
|
+
const filtered = filterFindings(session.findings, {
|
|
61
|
+
minSeverity: sev.value,
|
|
62
|
+
});
|
|
63
|
+
const groups = groupFindings(filtered, groupBy);
|
|
64
|
+
const limit = typeof args.limit === "number" ? args.limit : groups.length;
|
|
65
|
+
const top = groups.slice(0, limit);
|
|
66
|
+
const result = {
|
|
67
|
+
groupBy,
|
|
68
|
+
total: groups.length,
|
|
69
|
+
returned: top.length,
|
|
70
|
+
groups: top.map((g) => ({
|
|
71
|
+
key: g.key,
|
|
72
|
+
count: g.findings.length,
|
|
73
|
+
findingIds: g.findings.map((f) => f.id),
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
77
|
+
},
|
|
78
|
+
};
|