@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.
@@ -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 };