@okfit/mcp 0.1.0 → 0.3.0
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/README.md +64 -3
- package/bin/okfit-mcp.js +3 -4
- package/errors.js +105 -0
- package/index.d.ts +722 -8
- package/index.js +9 -20
- package/internal/projectRoot.js +14 -0
- package/internal/resolveNow.js +26 -0
- package/internal/toolContext.js +77 -0
- package/main.d.ts +20 -0
- package/main.js +45 -0
- package/package.json +13 -3
- package/resources/conceptResource.js +63 -0
- package/resources/indexResource.js +49 -0
- package/schema/ConceptSummary.js +27 -0
- package/schema/tools.js +78 -0
- package/server.js +35 -0
- package/toolkit.js +34 -0
- package/tools/conceptNeighbors.js +87 -0
- package/tools/describeVocabulary.js +73 -0
- package/tools/getConcept.js +93 -0
- package/tools/listConcepts.js +81 -0
- package/tools/staleReport.js +53 -0
- package/tools/validateBundle.js +94 -0
- package/version.js +11 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { McpToolError } from "../errors.js";
|
|
2
|
+
import { resolveConfigOnly } from "../internal/toolContext.js";
|
|
3
|
+
import { DescribeVocabularySuccess } from "../schema/tools.js";
|
|
4
|
+
import { Effect, FileSystem, Option, Path } from "effect";
|
|
5
|
+
import { Tool } from "effect/unstable/ai";
|
|
6
|
+
import { contextEnvelope } from "@okfit/engine";
|
|
7
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
8
|
+
|
|
9
|
+
//#region src/tools/describeVocabulary.ts
|
|
10
|
+
const DESCRIPTION = "Returns this project's resolved okfit configuration: the project root, bundle root, active profile, configured agent actor, and the full set of concept types and tags the config declares, each with its description. Call this first, before filtering or writing any concept, to learn which type and tag names actually exist in this project.";
|
|
11
|
+
/**
|
|
12
|
+
* `dependencies` names the platform services `handleDescribeVocabulary`
|
|
13
|
+
* needs (`resolveConfigOnly` -> `resolveProjectConfig`/`provideConfig`);
|
|
14
|
+
* without it `Tool.HandlerServices` infers `never` and the handler record
|
|
15
|
+
* passed to `Toolkit.toLayer` fails to typecheck against `HandlersFrom`
|
|
16
|
+
* (verified against `.repos/effect/packages/effect/src/unstable/ai/Tool.ts`
|
|
17
|
+
* `dependencies`/`HandlerServices`; not spelled out in the brief's snippet).
|
|
18
|
+
*
|
|
19
|
+
* `parameters` is `Tool.EmptyParams`, not the brief's `Schema.Struct({})`:
|
|
20
|
+
* a zero-key `Schema.Struct` fails `McpServer.layerStdio`'s build with a
|
|
21
|
+
* `SchemaError` (`MissingKey` at path `type`) when the toolkit registers
|
|
22
|
+
* its JSON Schema, reproduced in isolation against a minimal tool and
|
|
23
|
+
* confirmed fixed by switching to `Tool.EmptyParams` — the same schema
|
|
24
|
+
* Effect's own MCP conformance fixtures use for parameterless tools
|
|
25
|
+
* (`.repos/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformanceFixtures.ts:29`).
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
const describeVocabulary = Tool.make("describe_vocabulary", {
|
|
30
|
+
description: DESCRIPTION,
|
|
31
|
+
parameters: Tool.EmptyParams,
|
|
32
|
+
success: DescribeVocabularySuccess,
|
|
33
|
+
failure: McpToolError,
|
|
34
|
+
dependencies: [
|
|
35
|
+
FileSystem.FileSystem,
|
|
36
|
+
Path.Path,
|
|
37
|
+
AppDirs,
|
|
38
|
+
Xdg
|
|
39
|
+
]
|
|
40
|
+
}).annotate(Tool.Title, "Describe okfit vocabulary").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
41
|
+
/** @public */
|
|
42
|
+
const handleDescribeVocabulary = (projectRoot) => Effect.gen(function* () {
|
|
43
|
+
const resolved = yield* resolveConfigOnly(projectRoot);
|
|
44
|
+
const envelope = contextEnvelope({
|
|
45
|
+
projectRoot: resolved.projectRoot,
|
|
46
|
+
bundleRoot: resolved.bundleRoot,
|
|
47
|
+
configPath: Option.match(resolved.discovered, {
|
|
48
|
+
onNone: () => null,
|
|
49
|
+
onSome: (d) => d.path
|
|
50
|
+
}),
|
|
51
|
+
profile: Option.match(resolved.profile, {
|
|
52
|
+
onNone: () => null,
|
|
53
|
+
onSome: (p) => p.name
|
|
54
|
+
}),
|
|
55
|
+
profileRequested: resolved.profileName,
|
|
56
|
+
indexPath: "",
|
|
57
|
+
indexExists: false,
|
|
58
|
+
config: resolved.config
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
project_root: envelope.project_root,
|
|
62
|
+
bundle_root: envelope.bundle_root,
|
|
63
|
+
config_path: envelope.config_path,
|
|
64
|
+
profile: envelope.profile,
|
|
65
|
+
profile_requested: envelope.profile_requested,
|
|
66
|
+
agent: envelope.actors.agent,
|
|
67
|
+
types: envelope.types,
|
|
68
|
+
tags: envelope.tags
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { describeVocabulary, handleDescribeVocabulary };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ConceptNotFound, InvalidArgument, McpToolError, composeRemediatedMessage, truncateEchoed } from "../errors.js";
|
|
2
|
+
import { loadToolContext } from "../internal/toolContext.js";
|
|
3
|
+
import { GetConceptSuccess } from "../schema/tools.js";
|
|
4
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
5
|
+
import { ConceptId, Derive, Graph } from "@okfit/core";
|
|
6
|
+
import { Tool } from "effect/unstable/ai";
|
|
7
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
8
|
+
|
|
9
|
+
//#region src/tools/getConcept.ts
|
|
10
|
+
const DESCRIPTION = "Returns one concept by id: its whole decoded frontmatter, the file's raw markdown text, its bundle-relative path, and every outgoing link with where the link was written and whether the target is another concept, an existing file, or missing. Accepts a tolerant id, with or without a leading slash or a trailing .md.";
|
|
11
|
+
/**
|
|
12
|
+
* `dependencies` mirrors `list_concepts`' and `describe_vocabulary`'s
|
|
13
|
+
* (Task B1's Deviation 1, carried through Task C1): `loadToolContext` needs
|
|
14
|
+
* the same platform services, and without a `dependencies` declaration
|
|
15
|
+
* `Tool.HandlerServices` infers `never`, failing the handler record against
|
|
16
|
+
* `HandlersFrom` when it is passed to `OkfitToolkit.toLayer`.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const getConcept = Tool.make("get_concept", {
|
|
21
|
+
description: DESCRIPTION,
|
|
22
|
+
parameters: Schema.Struct({ id: Schema.String }),
|
|
23
|
+
success: GetConceptSuccess,
|
|
24
|
+
failure: McpToolError,
|
|
25
|
+
dependencies: [
|
|
26
|
+
FileSystem.FileSystem,
|
|
27
|
+
Path.Path,
|
|
28
|
+
AppDirs,
|
|
29
|
+
Xdg
|
|
30
|
+
]
|
|
31
|
+
}).annotate(Tool.Title, "Get one OKF concept").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
32
|
+
/**
|
|
33
|
+
* Deviation from the brief's literal snippet, matching the ruling binding
|
|
34
|
+
* every C task (progress.md): `message` is composed through
|
|
35
|
+
* {@link composeRemediatedMessage} at construction, since a declared typed
|
|
36
|
+
* failure under `failureMode: "error"` never reaches the wire with
|
|
37
|
+
* `structuredContent` — only `error.message` does.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
const handleGetConcept = (projectRoot, params) => Effect.gen(function* () {
|
|
42
|
+
const ctx = yield* loadToolContext(projectRoot);
|
|
43
|
+
const normalized = ConceptId.normalize(params.id);
|
|
44
|
+
if (Option.isNone(normalized)) {
|
|
45
|
+
const remediation = {
|
|
46
|
+
hint: "Pass a bundle-relative concept id such as decisions/cli-exit-codes.",
|
|
47
|
+
suggestedTool: "list_concepts"
|
|
48
|
+
};
|
|
49
|
+
return yield* Effect.fail(new InvalidArgument({
|
|
50
|
+
argument: "id",
|
|
51
|
+
message: composeRemediatedMessage("id must not be empty", remediation),
|
|
52
|
+
remediation
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const id = normalized.value;
|
|
56
|
+
const concept = ctx.bundle.concepts.get(id);
|
|
57
|
+
if (concept === void 0) {
|
|
58
|
+
const remediation = {
|
|
59
|
+
hint: "Call list_concepts to see the ids this bundle contains.",
|
|
60
|
+
suggestedTool: "list_concepts"
|
|
61
|
+
};
|
|
62
|
+
return yield* Effect.fail(new ConceptNotFound({
|
|
63
|
+
id: params.id,
|
|
64
|
+
message: composeRemediatedMessage(`no concept "${truncateEchoed(params.id)}" in this bundle`, remediation),
|
|
65
|
+
remediation
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
const graph = Graph.fromBundle(ctx.bundle);
|
|
69
|
+
const links = graph.edges.filter((edge) => edge.from === id).map((edge) => ({
|
|
70
|
+
to: edge.to,
|
|
71
|
+
kind: Option.match(graph.node(edge.to), {
|
|
72
|
+
onNone: () => "missing",
|
|
73
|
+
onSome: (node) => node.kind
|
|
74
|
+
}),
|
|
75
|
+
source: edge.data.source,
|
|
76
|
+
...edge.data.field === void 0 ? {} : { field: edge.data.field }
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
type: concept.frontmatter.type,
|
|
81
|
+
title: Derive.title(concept),
|
|
82
|
+
description: concept.frontmatter.description ?? null,
|
|
83
|
+
status: Derive.status(concept.frontmatter),
|
|
84
|
+
tags: [...concept.frontmatter.tags ?? []],
|
|
85
|
+
path: concept.path,
|
|
86
|
+
frontmatter: concept.frontmatter.raw,
|
|
87
|
+
raw: concept.document.source,
|
|
88
|
+
links
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { getConcept, handleGetConcept };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { McpToolError, UnknownVocabulary, composeRemediatedMessage, truncateEchoed } from "../errors.js";
|
|
2
|
+
import { loadToolContext } from "../internal/toolContext.js";
|
|
3
|
+
import { toConceptSummary } from "../schema/ConceptSummary.js";
|
|
4
|
+
import { ListConceptsParams, ListConceptsSuccess } from "../schema/tools.js";
|
|
5
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
6
|
+
import { Derive } from "@okfit/core";
|
|
7
|
+
import { Tool } from "effect/unstable/ai";
|
|
8
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
9
|
+
|
|
10
|
+
//#region src/tools/listConcepts.ts
|
|
11
|
+
const DESCRIPTION = "Lists concept summaries from the okf bundle, optionally filtered by an exact type, by tags that must all be present (AND, not OR), and by status. Pages with limit (default 200, max 1000) and offset, and reports the total matching count. Use it to discover what exists before reading one concept with get_concept; an unknown type or tag name fails with the list of valid names.";
|
|
12
|
+
/**
|
|
13
|
+
* `dependencies` mirrors `describe_vocabulary`'s (Deviation 1 in Task B1's
|
|
14
|
+
* report): `loadToolContext` needs the same platform services
|
|
15
|
+
* (`resolveConfigOnly` -> `resolveProjectConfig`/`provideConfig`, plus
|
|
16
|
+
* `Bundle.load`), and without a `dependencies` declaration
|
|
17
|
+
* `Tool.HandlerServices` infers `never`, failing the handler record against
|
|
18
|
+
* `HandlersFrom` when it is passed to `OkfitToolkit.toLayer`.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const listConcepts = Tool.make("list_concepts", {
|
|
23
|
+
description: DESCRIPTION,
|
|
24
|
+
parameters: ListConceptsParams,
|
|
25
|
+
success: ListConceptsSuccess,
|
|
26
|
+
failure: McpToolError,
|
|
27
|
+
dependencies: [
|
|
28
|
+
FileSystem.FileSystem,
|
|
29
|
+
Path.Path,
|
|
30
|
+
AppDirs,
|
|
31
|
+
Xdg
|
|
32
|
+
]
|
|
33
|
+
}).annotate(Tool.Title, "List OKF concepts").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
34
|
+
/**
|
|
35
|
+
* Deviation from the brief's literal snippet: `message` is composed through
|
|
36
|
+
* {@link composeRemediatedMessage} (per the controller's ruling binding every
|
|
37
|
+
* C task, progress.md), and the raw message embeds the full `valid` list
|
|
38
|
+
* rather than leaving it only on the schema's `valid` field — a declared
|
|
39
|
+
* typed failure under `failureMode: "error"` never reaches the wire with
|
|
40
|
+
* `structuredContent` (B1's finding), so `valid` would otherwise be
|
|
41
|
+
* unreachable to a real caller reading only `content[0].text`.
|
|
42
|
+
*/
|
|
43
|
+
const unknown = (kind, requested, valid) => {
|
|
44
|
+
const remediation = {
|
|
45
|
+
hint: "Use one of the listed type names, or call describe_vocabulary.",
|
|
46
|
+
suggestedTool: "describe_vocabulary"
|
|
47
|
+
};
|
|
48
|
+
const rawMessage = `"${truncateEchoed(requested)}" is not a ${kind} declared by this project's okfit config. Valid ${kind}s: ${valid.join(", ")}.`;
|
|
49
|
+
return new UnknownVocabulary({
|
|
50
|
+
kind,
|
|
51
|
+
requested,
|
|
52
|
+
valid,
|
|
53
|
+
message: composeRemediatedMessage(rawMessage, remediation),
|
|
54
|
+
remediation
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
/** @public */
|
|
58
|
+
const handleListConcepts = (projectRoot, params) => Effect.gen(function* () {
|
|
59
|
+
const ctx = yield* loadToolContext(projectRoot);
|
|
60
|
+
const declaredTypes = Object.keys(ctx.config.types ?? {}).toSorted();
|
|
61
|
+
const declaredTags = Object.keys(ctx.config.tags ?? {}).toSorted();
|
|
62
|
+
if (params.type !== void 0 && !declaredTypes.includes(params.type)) return yield* Effect.fail(unknown("type", params.type, declaredTypes));
|
|
63
|
+
const requestedTags = params.tags ?? [];
|
|
64
|
+
for (const tag of requestedTags) if (!declaredTags.includes(tag)) return yield* Effect.fail(unknown("tag", tag, declaredTags));
|
|
65
|
+
const filtered = [...ctx.bundle.concepts.values()].filter((concept) => {
|
|
66
|
+
if (params.type !== void 0 && concept.frontmatter.type !== params.type) return false;
|
|
67
|
+
const tags = concept.frontmatter.tags ?? [];
|
|
68
|
+
if (!requestedTags.every((tag) => tags.includes(tag))) return false;
|
|
69
|
+
if (params.status !== void 0 && Derive.status(concept.frontmatter) !== params.status) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}).toSorted((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
72
|
+
const limit = params.limit ?? 200;
|
|
73
|
+
const offset = params.offset ?? 0;
|
|
74
|
+
return {
|
|
75
|
+
items: filtered.slice(offset, offset + limit).map(toConceptSummary),
|
|
76
|
+
total: filtered.length
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { handleListConcepts, listConcepts };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { McpToolError } from "../errors.js";
|
|
2
|
+
import { loadToolContext } from "../internal/toolContext.js";
|
|
3
|
+
import { toConceptSummary } from "../schema/ConceptSummary.js";
|
|
4
|
+
import { StaleReportParams, StaleReportSuccess } from "../schema/tools.js";
|
|
5
|
+
import { resolveNow } from "../internal/resolveNow.js";
|
|
6
|
+
import { DateTime, Effect, FileSystem, Path } from "effect";
|
|
7
|
+
import { Derive } from "@okfit/core";
|
|
8
|
+
import { Tool } from "effect/unstable/ai";
|
|
9
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
10
|
+
|
|
11
|
+
//#region src/tools/staleReport.ts
|
|
12
|
+
const DESCRIPTION = "Lists every concept whose stale_after instant has passed, each with its summary, the date, and how many whole days past it. Uses the current time by default; pass now as an ISO-8601 instant with an explicit offset to report staleness as of a different moment.";
|
|
13
|
+
/**
|
|
14
|
+
* `dependencies` mirrors the other tools' (Task B1's Deviation 1):
|
|
15
|
+
* `loadToolContext` needs the same platform services, and without a
|
|
16
|
+
* `dependencies` declaration `Tool.HandlerServices` infers `never`, failing
|
|
17
|
+
* the handler record against `HandlersFrom` when passed to
|
|
18
|
+
* `OkfitToolkit.toLayer`.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const staleReport = Tool.make("stale_report", {
|
|
23
|
+
description: DESCRIPTION,
|
|
24
|
+
parameters: StaleReportParams,
|
|
25
|
+
success: StaleReportSuccess,
|
|
26
|
+
failure: McpToolError,
|
|
27
|
+
dependencies: [
|
|
28
|
+
FileSystem.FileSystem,
|
|
29
|
+
Path.Path,
|
|
30
|
+
AppDirs,
|
|
31
|
+
Xdg
|
|
32
|
+
]
|
|
33
|
+
}).annotate(Tool.Title, "Stale concepts").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
34
|
+
/** @public */
|
|
35
|
+
const handleStaleReport = (projectRoot, params) => Effect.gen(function* () {
|
|
36
|
+
const ctx = yield* loadToolContext(projectRoot);
|
|
37
|
+
const now = yield* resolveNow(params.now);
|
|
38
|
+
const items = Derive.staleReport(ctx.bundle, now).flatMap((entry) => {
|
|
39
|
+
const concept = ctx.bundle.concepts.get(entry.id);
|
|
40
|
+
return concept === void 0 ? [] : [{
|
|
41
|
+
summary: toConceptSummary(concept),
|
|
42
|
+
stale_after: DateTime.formatIso(entry.staleAfter),
|
|
43
|
+
days_past: entry.daysPast
|
|
44
|
+
}];
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
as_of: DateTime.formatIso(now),
|
|
48
|
+
items
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { handleStaleReport, staleReport };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BundleNotFound, McpToolError, composeRemediatedMessage } from "../errors.js";
|
|
2
|
+
import { resolveConfigOnly } from "../internal/toolContext.js";
|
|
3
|
+
import { ValidateBundleParams } from "../schema/tools.js";
|
|
4
|
+
import { resolveNow } from "../internal/resolveNow.js";
|
|
5
|
+
import { MCP_VERSION } from "../version.js";
|
|
6
|
+
import { Effect, FileSystem, Option, Path } from "effect";
|
|
7
|
+
import { OKF_SPEC_VERSION } from "@okfit/core";
|
|
8
|
+
import { Tool } from "effect/unstable/ai";
|
|
9
|
+
import { JsonEnvelope, collect, forDiagnostics, json, run } from "@okfit/engine";
|
|
10
|
+
import { Git } from "@effected/git";
|
|
11
|
+
import { GitHistory } from "@okfit/profiles";
|
|
12
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
13
|
+
|
|
14
|
+
//#region src/tools/validateBundle.ts
|
|
15
|
+
const DESCRIPTION = "Runs the same conformance and lint checks as `okfit validate --format json` and returns that report unchanged: every diagnostic's file, code, severity, message and range, plus the summary counts and the exit code the CLI would use. Call it after writing or editing any concept file, before moving on to the next one.";
|
|
16
|
+
/**
|
|
17
|
+
* `dependencies` mirrors the other tools' (Task B1's Deviation 1):
|
|
18
|
+
* `resolveConfigOnly` and `run()` need the same platform services, and
|
|
19
|
+
* without a `dependencies` declaration `Tool.HandlerServices` infers
|
|
20
|
+
* `never`, failing the handler record against `HandlersFrom` when passed to
|
|
21
|
+
* `OkfitToolkit.toLayer`.
|
|
22
|
+
*
|
|
23
|
+
* `Git` and `GitHistory` are new in this list: B3 widened `run()`'s own
|
|
24
|
+
* requirement channel to `FileSystem.FileSystem | Path.Path | Git |
|
|
25
|
+
* GitHistory` so it can run `Provenance.lint`'s `generated-at-drift`
|
|
26
|
+
* check (S-8, S-16). Both are provided by `server.ts`'s `ServerLayer`,
|
|
27
|
+
* which needs only `ChildProcessSpawner` to build them — already
|
|
28
|
+
* supplied by `@okfit/engine`'s `OkfitPlatform` (S-16).
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
const validateBundle = Tool.make("validate_bundle", {
|
|
33
|
+
description: DESCRIPTION,
|
|
34
|
+
parameters: ValidateBundleParams,
|
|
35
|
+
success: JsonEnvelope,
|
|
36
|
+
failure: McpToolError,
|
|
37
|
+
dependencies: [
|
|
38
|
+
FileSystem.FileSystem,
|
|
39
|
+
Path.Path,
|
|
40
|
+
AppDirs,
|
|
41
|
+
Xdg,
|
|
42
|
+
Git,
|
|
43
|
+
GitHistory
|
|
44
|
+
]
|
|
45
|
+
}).annotate(Tool.Title, "Validate the bundle").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
46
|
+
/**
|
|
47
|
+
* This tool does not call `loadToolContext`: `run()` calls `Bundle.load`
|
|
48
|
+
* itself (`packages/engine/src/validate/run.ts:52-56`), so routing through
|
|
49
|
+
* `loadToolContext` would load the bundle twice. It uses `resolveConfigOnly`
|
|
50
|
+
* and keeps `bundleRoot`, `config` **and** `profile` — `run()` needs the
|
|
51
|
+
* profile. `success` is `JsonEnvelope`, imported from `@okfit/engine` and used
|
|
52
|
+
* verbatim (N-35's own carve-out: data reuse, not envelope reuse);
|
|
53
|
+
* `JsonErrorEnvelope` is never returned — that is the CLI's stdout
|
|
54
|
+
* convention for an exit-3 infrastructure failure, and here that failure is
|
|
55
|
+
* a typed `McpToolError` instead, so `forDiagnostics`' `0 | 1 | 2` result is
|
|
56
|
+
* always reachable. `okfitVersion` is `MCP_VERSION`, not `CLI_VERSION`
|
|
57
|
+
* (J-2): the field names the package that produced the report.
|
|
58
|
+
*
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
const handleValidateBundle = (projectRoot, params) => Effect.gen(function* () {
|
|
62
|
+
const resolved = yield* resolveConfigOnly(projectRoot);
|
|
63
|
+
const now = yield* resolveNow(params.now);
|
|
64
|
+
const result = yield* run({
|
|
65
|
+
root: resolved.bundleRoot,
|
|
66
|
+
config: resolved.config,
|
|
67
|
+
profile: resolved.profile,
|
|
68
|
+
now
|
|
69
|
+
}).pipe(Effect.mapError((cause) => {
|
|
70
|
+
const remediation = { hint: `The bundle root "${resolved.bundleRoot}" does not exist or could not be read; check the config's [bundle].path, or run \`okfit init\`.` };
|
|
71
|
+
return new BundleNotFound({
|
|
72
|
+
root: resolved.bundleRoot,
|
|
73
|
+
message: composeRemediatedMessage(cause.message, remediation),
|
|
74
|
+
remediation
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
const diagnostics = collect(result.report.conformance, result.report.lint, result.profileDiagnostics);
|
|
78
|
+
const code = forDiagnostics(diagnostics);
|
|
79
|
+
return json({
|
|
80
|
+
okfitVersion: MCP_VERSION,
|
|
81
|
+
okfVersion: resolved.config.okf_version ?? OKF_SPEC_VERSION,
|
|
82
|
+
root: resolved.bundleRoot,
|
|
83
|
+
profile: Option.match(resolved.profile, {
|
|
84
|
+
onNone: () => null,
|
|
85
|
+
onSome: (profile) => profile.name
|
|
86
|
+
}),
|
|
87
|
+
exitCode: code,
|
|
88
|
+
concepts: result.bundle.concepts.size,
|
|
89
|
+
diagnostics
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { handleValidateBundle, validateBundle };
|
package/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/version.ts
|
|
2
|
+
/**
|
|
3
|
+
* The version this server reports in `initialize`. Injected by
|
|
4
|
+
* `@savvy-web/bundler` at build time (K-32); `"0.0.0"` in unbuilt source.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
const MCP_VERSION = "0.3.0";
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { MCP_VERSION };
|