@okfit/mcp 0.1.0 → 0.2.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/index.js CHANGED
@@ -1,21 +1,10 @@
1
- //#region src/index.ts
2
- /**
3
- * Model Context Protocol server for okfit.
4
- *
5
- * @packageDocumentation
6
- */
7
- /**
8
- * Message the stub bin writes to stderr until the server exists.
9
- *
10
- * @public
11
- */
12
- const NOT_IMPLEMENTED_MESSAGE = "okfit-mcp: the MCP server is not implemented yet. Track progress at https://github.com/spencerbeggs/okfit.";
13
- /**
14
- * Exit code the stub bin uses.
15
- *
16
- * @public
17
- */
18
- const NOT_IMPLEMENTED_EXIT_CODE = 1;
1
+ import { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary, composeRemediatedMessage, truncateEchoed } from "./errors.js";
2
+ import { ConceptResources } from "./resources/conceptResource.js";
3
+ import { IndexResource } from "./resources/indexResource.js";
4
+ import { ConceptSummary, toConceptSummary } from "./schema/ConceptSummary.js";
5
+ import { ConceptNeighborsSuccess, DescribeVocabularySuccess, GetConceptSuccess, ListConceptsParams, ListConceptsSuccess, Neighbor, StaleReportParams, StaleReportSuccess, ValidateBundleParams } from "./schema/tools.js";
6
+ import { MCP_VERSION } from "./version.js";
7
+ import { OkfitToolkit, ToolsLayer } from "./toolkit.js";
8
+ import { ServerLayer } from "./server.js";
19
9
 
20
- //#endregion
21
- export { NOT_IMPLEMENTED_EXIT_CODE, NOT_IMPLEMENTED_MESSAGE };
10
+ export { BundleNotFound, ConceptNeighborsSuccess, ConceptNotFound, ConceptResources, ConceptSummary, ConfigError, DescribeVocabularySuccess, GetConceptSuccess, IndexResource, InvalidArgument, ListConceptsParams, ListConceptsSuccess, MCP_VERSION, McpToolError, Neighbor, OkfitToolkit, Remediation, ServerLayer, StaleReportParams, StaleReportSuccess, ToolsLayer, UnknownVocabulary, ValidateBundleParams, composeRemediatedMessage, toConceptSummary, truncateEchoed };
@@ -0,0 +1,14 @@
1
+ //#region src/internal/projectRoot.ts
2
+ /**
3
+ * N-21's precedence, exactly: `OKFIT_PROJECT_DIR`, else
4
+ * `CLAUDE_PROJECT_DIR`, else the process's working directory. Pure over
5
+ * its `env` argument so tests never touch `process.env`. No argv flags:
6
+ * `start-mcp.sh` forwards `"$@"` with nothing in it, and the manifest's
7
+ * `cwd` field is unused.
8
+ *
9
+ * @internal
10
+ */
11
+ const resolveMcpProjectRoot = (env) => env["OKFIT_PROJECT_DIR"] ?? env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
12
+
13
+ //#endregion
14
+ export { resolveMcpProjectRoot };
@@ -0,0 +1,26 @@
1
+ import { InvalidArgument, composeRemediatedMessage } from "../errors.js";
2
+ import { DateTime, Effect, Schema } from "effect";
3
+ import { Timestamp } from "@okfit/core";
4
+
5
+ //#region src/internal/resolveNow.ts
6
+ /**
7
+ * An optional ISO-8601 `now` argument, else the Effect clock. Decoding
8
+ * happens here rather than in the parameter schema so a malformed value
9
+ * becomes this contract's `InvalidArgument` — with a remediation hint —
10
+ * instead of Effect's generic protocol-level `InvalidParams` (J-5).
11
+ * `OKFIT_NOW` is deliberately NOT read: it is the CLI's own test hook
12
+ * (F-17), never the server's.
13
+ *
14
+ * @internal
15
+ */
16
+ const resolveNow = (input) => input === void 0 ? DateTime.now : Schema.decodeUnknownEffect(Timestamp)(input).pipe(Effect.mapError((issue) => {
17
+ const remediation = { hint: "now must be an ISO-8601 instant with an explicit offset, for example 2026-09-06T00:00:00Z." };
18
+ return new InvalidArgument({
19
+ argument: "now",
20
+ message: composeRemediatedMessage(String(issue), remediation),
21
+ remediation
22
+ });
23
+ }));
24
+
25
+ //#endregion
26
+ export { resolveNow };
@@ -0,0 +1,77 @@
1
+ import { BundleNotFound, ConfigError, composeRemediatedMessage } from "../errors.js";
2
+ import { Effect, Option } from "effect";
3
+ import { Bundle } from "@okfit/core";
4
+ import { provideConfig, resolveProjectConfig } from "@okfit/cli";
5
+
6
+ //#region src/internal/toolContext.ts
7
+ /**
8
+ * The error's `message` when it has one as a string, else `String(error)`.
9
+ * A four-line local copy of `CLI/render/json.ts`'s own helper, which is
10
+ * module-private there and cannot be imported.
11
+ */
12
+ const messageOf = (error) => {
13
+ if (typeof error === "object" && error !== null && "message" in error) {
14
+ const message = error.message;
15
+ if (typeof message === "string") return message;
16
+ }
17
+ return String(error);
18
+ };
19
+ const CONFIG_HINT = "Check the project's .okfit.toml, okfit.toml, or .config/okfit.toml for a syntax or schema error; remove it to fall back to defaults.";
20
+ /**
21
+ * Config resolution alone, with every failure collapsed to `ConfigError`.
22
+ * `describe_vocabulary` and `validate_bundle` use this; every other tool
23
+ * goes through {@link loadToolContext}.
24
+ *
25
+ * The requirement union (`FileSystem.FileSystem | Path.Path | AppDirs | Xdg`)
26
+ * is inlined here rather than named, so API Extractor never needs a local
27
+ * type alias exported from the package's entry point (K-32-adjacent; a
28
+ * private `ToolServices` alias failed `ae-forgotten-export` at the entry
29
+ * point in this task's own build).
30
+ *
31
+ * @internal
32
+ */
33
+ const resolveConfigOnly = (projectRoot) => resolveProjectConfig({
34
+ pathArg: Option.none(),
35
+ explicitConfigPath: Option.none(),
36
+ cwd: projectRoot
37
+ }).pipe(provideConfig({
38
+ explicitConfigPath: Option.none(),
39
+ discoveryCwd: projectRoot
40
+ }), Effect.mapError((cause) => {
41
+ const remediation = {
42
+ hint: CONFIG_HINT,
43
+ suggestedTool: "describe_vocabulary"
44
+ };
45
+ return new ConfigError({
46
+ message: composeRemediatedMessage(messageOf(cause), remediation),
47
+ remediation
48
+ });
49
+ }));
50
+ /**
51
+ * Resolve config and load the bundle, fresh, for one tool call (N-9). No
52
+ * cache and no `reload` tool: correct under mid-session edits, trivially
53
+ * testable, and the bundles in scope are small.
54
+ *
55
+ * @internal
56
+ */
57
+ const loadToolContext = (projectRoot) => Effect.gen(function* () {
58
+ const resolved = yield* resolveConfigOnly(projectRoot);
59
+ const bundle = yield* Bundle.load({ root: resolved.bundleRoot }).pipe(Effect.mapError((cause) => {
60
+ 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\`.` };
61
+ return new BundleNotFound({
62
+ root: resolved.bundleRoot,
63
+ message: composeRemediatedMessage(cause.message, remediation),
64
+ remediation
65
+ });
66
+ }));
67
+ return {
68
+ projectRoot,
69
+ bundleRoot: resolved.bundleRoot,
70
+ config: resolved.config,
71
+ profile: resolved.profile,
72
+ bundle
73
+ };
74
+ });
75
+
76
+ //#endregion
77
+ export { loadToolContext, messageOf, resolveConfigOnly };
package/package.js ADDED
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var version = "0.2.0";
3
+
4
+ //#endregion
5
+ export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Model Context Protocol server for okfit: query and understand Open Knowledge Format (OKF) bundles from an agent.",
6
6
  "keywords": [
@@ -39,8 +39,13 @@
39
39
  "okfit-mcp": "bin/okfit-mcp.js"
40
40
  },
41
41
  "dependencies": {
42
- "@okfit/core": "0.1.0",
43
- "effect": "4.0.0-rc.109"
42
+ "@effect/platform-node": "4.0.0-rc.112",
43
+ "@effected/git": "^0.12.0",
44
+ "@effected/xdg": "^0.4.1",
45
+ "@okfit/cli": "0.2.0",
46
+ "@okfit/core": "0.2.0",
47
+ "@okfit/profiles": "0.2.0",
48
+ "effect": "4.0.0-rc.112"
44
49
  },
45
50
  "engines": {
46
51
  "node": ">=24.11.0"
@@ -0,0 +1,63 @@
1
+ import { loadToolContext } from "../internal/toolContext.js";
2
+ import { Effect, FileSystem, Layer, Path } from "effect";
3
+ import { Derive } from "@okfit/core";
4
+ import { McpSchema, McpServer } from "effect/unstable/ai";
5
+
6
+ //#region src/resources/conceptResource.ts
7
+ /**
8
+ * `okf://concept/<id>` — one static resource per concept in the bundle,
9
+ * built once at server start.
10
+ *
11
+ * Amends N-19 and contract §6.1 (Ruling, task C4): a `McpServer.resource`
12
+ * URI **template** routes a `McpSchema.param` through a single path
13
+ * segment only — `FindMyWay`'s parametric matcher stops a param at the
14
+ * next `/` (`.repos/effect/packages/effect/src/unstable/http/FindMyWay/internal/router.ts:355-360`),
15
+ * and every non-root OKF concept id nests under a type directory (D-12),
16
+ * e.g. `metrics/revenue`. A templated `okf://concept/{id}` therefore never
17
+ * routes a real bundle id. Static per-concept resources sidestep the
18
+ * router entirely: each concept gets its own literal `uri`, so there is
19
+ * no segment-spanning to fail.
20
+ *
21
+ * The list is fixed at server start (`loadToolContext` runs once, inside
22
+ * `Layer.unwrap`, not per read): a concept added after boot is not listed
23
+ * until the server restarts, though editing an already-listed concept's
24
+ * file is picked up live, since `content` re-reads the file from disk on
25
+ * every read (same complete text `get_concept`'s `raw` field returns).
26
+ *
27
+ * If the bundle fails to load at boot (bad config, missing bundle root),
28
+ * one line is logged and this layer contributes no resources — the
29
+ * six tools stay usable; only `okf://index` remains, since it is
30
+ * registered independently and re-resolves the bundle on every read.
31
+ *
32
+ * `content` builds the full `ReadResourceResult` itself, `mimeType`
33
+ * included, rather than returning a bare string: verified against source
34
+ * (`resolveResourceContent`, `unstable/ai/McpServer.ts:2324-2343`), a bare
35
+ * string is wrapped as `{ contents: [{ uri, text }] }` with no `mimeType`
36
+ * at all — the declared `mimeType` option only documents the resource in
37
+ * `resources/list`, it is never merged into a `resources/read` response.
38
+ *
39
+ * @public
40
+ */
41
+ const ConceptResources = (projectRoot) => Layer.unwrap(Effect.gen(function* () {
42
+ const ctx = yield* loadToolContext(projectRoot);
43
+ const path = yield* Path.Path;
44
+ const fs = yield* FileSystem.FileSystem;
45
+ return [...ctx.bundle.concepts.entries()].map(([id, concept]) => {
46
+ const uri = `okf://concept/${id}`;
47
+ const absolutePath = path.join(ctx.bundleRoot, concept.path);
48
+ return McpServer.resource({
49
+ uri,
50
+ name: id,
51
+ description: Derive.title(concept),
52
+ mimeType: "text/markdown",
53
+ content: fs.readFileString(absolutePath).pipe(Effect.map((text) => ({ contents: [{
54
+ uri,
55
+ mimeType: "text/markdown",
56
+ text
57
+ }] })), Effect.mapError(() => new McpSchema.InternalError({ message: `no readable concept file at ${absolutePath}` })))
58
+ });
59
+ }).reduce((acc, layer) => acc.pipe(Layer.merge(layer)), Layer.empty);
60
+ }).pipe(Effect.tapError((error) => Effect.logError(`okfit-mcp: could not load the bundle to register concept resources: ${error.message}`)), Effect.orElseSucceed(() => Layer.empty)));
61
+
62
+ //#endregion
63
+ export { ConceptResources };
@@ -0,0 +1,49 @@
1
+ import { loadToolContext } from "../internal/toolContext.js";
2
+ import { Effect, FileSystem, Path } from "effect";
3
+ import { McpSchema, McpServer } from "effect/unstable/ai";
4
+
5
+ //#region src/resources/indexResource.ts
6
+ /**
7
+ * `okf://index` — the bundle's root `index.md`, read from disk.
8
+ *
9
+ * This reads the file from disk, deliberately (J-7): `IndexDocument`
10
+ * carries only `{ path, dir, okfVersion?, sections }` — no raw-source
11
+ * field at all — so `bundle.indexes.get("")` cannot supply the raw
12
+ * markdown N-19 asks for. Reading the file also means a root `index.md`
13
+ * that failed to parse is still served, which is the more useful
14
+ * behaviour for an agent trying to fix it.
15
+ *
16
+ * A missing `index.md` fails the read rather than returning placeholder
17
+ * text (J-8): a resource that silently returns prose instead of the file
18
+ * it names is indistinguishable, to a model, from a bundle whose index
19
+ * really says that; §6.1 already fails an unknown id through the same
20
+ * channel.
21
+ *
22
+ * `content` builds the full `ReadResourceResult` itself, `mimeType`
23
+ * included, rather than returning a bare string: verified against source
24
+ * (`resolveResourceContent`, `unstable/ai/McpServer.ts:2324-2343`), a bare
25
+ * string is wrapped as `{ contents: [{ uri, text }] }` with no `mimeType`
26
+ * at all — the declared `mimeType` option only documents the resource in
27
+ * `resources/list`, it is never merged into a `resources/read` response.
28
+ *
29
+ * @public
30
+ */
31
+ const IndexResource = (projectRoot) => McpServer.resource({
32
+ uri: "okf://index",
33
+ name: "OKF bundle index",
34
+ description: "The bundle's root index.md — the entry point for orienting on this OKF bundle.",
35
+ mimeType: "text/markdown",
36
+ content: Effect.gen(function* () {
37
+ const ctx = yield* loadToolContext(projectRoot);
38
+ const fs = yield* FileSystem.FileSystem;
39
+ const indexPath = (yield* Path.Path).join(ctx.bundleRoot, "index.md");
40
+ return { contents: [{
41
+ uri: "okf://index",
42
+ mimeType: "text/markdown",
43
+ text: yield* fs.readFileString(indexPath).pipe(Effect.mapError(() => new McpSchema.InternalError({ message: `no readable index.md at ${indexPath}` })))
44
+ }] };
45
+ })
46
+ });
47
+
48
+ //#endregion
49
+ export { IndexResource };
@@ -0,0 +1,27 @@
1
+ import { Schema } from "effect";
2
+ import { Derive, Status } from "@okfit/core";
3
+
4
+ //#region src/schema/ConceptSummary.ts
5
+ /** The seven-field shape every list-like tool result carries (N-16). @public */
6
+ const ConceptSummary = Schema.Struct({
7
+ id: Schema.String,
8
+ type: Schema.String,
9
+ title: Schema.String,
10
+ description: Schema.NullOr(Schema.String),
11
+ status: Status,
12
+ tags: Schema.Array(Schema.String),
13
+ path: Schema.String
14
+ });
15
+ /** Project one loaded concept into its summary. @public */
16
+ const toConceptSummary = (concept) => ({
17
+ id: concept.id,
18
+ type: concept.frontmatter.type,
19
+ title: Derive.title(concept),
20
+ description: concept.frontmatter.description ?? null,
21
+ status: Derive.status(concept.frontmatter),
22
+ tags: [...concept.frontmatter.tags ?? []],
23
+ path: concept.path
24
+ });
25
+
26
+ //#endregion
27
+ export { ConceptSummary, toConceptSummary };
@@ -0,0 +1,78 @@
1
+ import { ConceptSummary } from "./ConceptSummary.js";
2
+ import { Schema } from "effect";
3
+ import { GraphNodeKind, Status } from "@okfit/core";
4
+ import { ContextTag, ContextType } from "@okfit/cli";
5
+
6
+ //#region src/schema/tools.ts
7
+ /** `describe_vocabulary`'s result (§5.2). @public */
8
+ const DescribeVocabularySuccess = Schema.Struct({
9
+ project_root: Schema.String,
10
+ bundle_root: Schema.String,
11
+ config_path: Schema.NullOr(Schema.String),
12
+ profile: Schema.NullOr(Schema.String),
13
+ profile_requested: Schema.NullOr(Schema.String),
14
+ agent: Schema.NullOr(Schema.String),
15
+ types: Schema.Array(ContextType),
16
+ tags: Schema.Array(ContextTag)
17
+ });
18
+ /** `list_concepts`' arguments (§5.3). @public */
19
+ const ListConceptsParams = Schema.Struct({
20
+ type: Schema.optionalKey(Schema.String),
21
+ tags: Schema.optionalKey(Schema.Array(Schema.String)),
22
+ status: Schema.optionalKey(Status),
23
+ limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({
24
+ minimum: 1,
25
+ maximum: 1e3
26
+ }))),
27
+ offset: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
28
+ });
29
+ /** `list_concepts`' result: the page, plus the match count before paging. @public */
30
+ const ListConceptsSuccess = Schema.Struct({
31
+ items: Schema.Array(ConceptSummary),
32
+ total: Schema.Int
33
+ });
34
+ /** `get_concept`'s result (§5.4). @public */
35
+ const GetConceptSuccess = Schema.Struct({
36
+ id: Schema.String,
37
+ type: Schema.String,
38
+ title: Schema.String,
39
+ description: Schema.NullOr(Schema.String),
40
+ status: Status,
41
+ tags: Schema.Array(Schema.String),
42
+ path: Schema.String,
43
+ frontmatter: Schema.Record(Schema.String, Schema.Unknown),
44
+ raw: Schema.String,
45
+ links: Schema.Array(Schema.Struct({
46
+ to: Schema.String,
47
+ kind: GraphNodeKind,
48
+ source: Schema.Literals(["body", "frontmatter"]),
49
+ field: Schema.optionalKey(Schema.String)
50
+ }))
51
+ });
52
+ /** One graph neighbour: flat, with a nullable summary (J-4). @public */
53
+ const Neighbor = Schema.Struct({
54
+ id: Schema.String,
55
+ kind: GraphNodeKind,
56
+ summary: Schema.NullOr(ConceptSummary)
57
+ });
58
+ /** `concept_neighbors`' result (§5.5). @public */
59
+ const ConceptNeighborsSuccess = Schema.Struct({
60
+ outgoing: Schema.Array(Neighbor),
61
+ incoming: Schema.Array(Neighbor)
62
+ });
63
+ /** `stale_report`'s arguments: an optional ISO instant, decoded in the handler (J-5). @public */
64
+ const StaleReportParams = Schema.Struct({ now: Schema.optionalKey(Schema.String) });
65
+ /** `stale_report`'s result (§5.6). @public */
66
+ const StaleReportSuccess = Schema.Struct({
67
+ as_of: Schema.String,
68
+ items: Schema.Array(Schema.Struct({
69
+ summary: ConceptSummary,
70
+ stale_after: Schema.String,
71
+ days_past: Schema.Int
72
+ }))
73
+ });
74
+ /** `validate_bundle`'s arguments: the same shape and decode path as stale_report. @public */
75
+ const ValidateBundleParams = Schema.Struct({ now: Schema.optionalKey(Schema.String) });
76
+
77
+ //#endregion
78
+ export { ConceptNeighborsSuccess, DescribeVocabularySuccess, GetConceptSuccess, ListConceptsParams, ListConceptsSuccess, Neighbor, StaleReportParams, StaleReportSuccess, ValidateBundleParams };
package/server.js ADDED
@@ -0,0 +1,35 @@
1
+ import { ConceptResources } from "./resources/conceptResource.js";
2
+ import { IndexResource } from "./resources/indexResource.js";
3
+ import { MCP_VERSION } from "./version.js";
4
+ import { OkfitToolkit, ToolsLayer } from "./toolkit.js";
5
+ import { Layer } from "effect";
6
+ import { McpProtocol, McpServer } from "effect/unstable/ai";
7
+ import { Git } from "@effected/git";
8
+ import { GitHistory } from "@okfit/profiles";
9
+
10
+ //#region src/server.ts
11
+ /**
12
+ * The whole server as one layer: the toolkit, one static resource per
13
+ * concept (`okf://concept/<id>`, built once at boot — see
14
+ * {@link ConceptResources}), and `okf://index` (re-read from disk on every
15
+ * call), over `McpServer.layerStdio`.
16
+ *
17
+ * `protocols` ships BOTH adapters, newest first (N-2). Array order is
18
+ * load-bearing: the protocol registry falls back to `protocols[0]` for an
19
+ * unrecognised client version, so a 2026-07-28 client is answered with
20
+ * 2025-11-25. Never reduce this to one entry.
21
+ *
22
+ * `Cause.IllegalArgumentError` in `layerStdio`'s signature is left
23
+ * unhandled: `protocols` is a static two-element literal, so it is an
24
+ * implementer-time defect, not a runtime condition.
25
+ *
26
+ * @public
27
+ */
28
+ const ServerLayer = (projectRoot) => Layer.mergeAll(McpServer.toolkit(OkfitToolkit).pipe(Layer.provideMerge(ToolsLayer(projectRoot))), ConceptResources(projectRoot), IndexResource(projectRoot)).pipe(Layer.provide(Layer.mergeAll(Git.layer, GitHistory.layer)), Layer.provide(McpServer.layerStdio({
29
+ name: "okfit",
30
+ version: MCP_VERSION,
31
+ protocols: [McpProtocol.v2025_11_25, McpProtocol.v2025_06_18]
32
+ })), Layer.orDie);
33
+
34
+ //#endregion
35
+ export { ServerLayer };
package/toolkit.js ADDED
@@ -0,0 +1,34 @@
1
+ import { conceptNeighbors, handleConceptNeighbors } from "./tools/conceptNeighbors.js";
2
+ import { describeVocabulary, handleDescribeVocabulary } from "./tools/describeVocabulary.js";
3
+ import { getConcept, handleGetConcept } from "./tools/getConcept.js";
4
+ import { handleListConcepts, listConcepts } from "./tools/listConcepts.js";
5
+ import { handleStaleReport, staleReport } from "./tools/staleReport.js";
6
+ import { handleValidateBundle, validateBundle } from "./tools/validateBundle.js";
7
+ import { Toolkit } from "effect/unstable/ai";
8
+
9
+ //#region src/toolkit.ts
10
+ /**
11
+ * The six read-only tools (N-10). Tasks C1, C2 and C3 each add two; the
12
+ * order here is the tool set's own order and is what
13
+ * `agents/okf-docs.md`'s `tools:` allowlist mirrors.
14
+ *
15
+ * @public
16
+ */
17
+ const OkfitToolkit = Toolkit.make(describeVocabulary, listConcepts, getConcept, conceptNeighbors, staleReport, validateBundle);
18
+ /**
19
+ * The handler layer. `projectRoot` is closed over from the bin (N-21);
20
+ * the bundle itself reloads on every call inside each handler (N-9).
21
+ *
22
+ * @public
23
+ */
24
+ const ToolsLayer = (projectRoot) => OkfitToolkit.toLayer({
25
+ describe_vocabulary: () => handleDescribeVocabulary(projectRoot),
26
+ list_concepts: (params) => handleListConcepts(projectRoot, params),
27
+ get_concept: (params) => handleGetConcept(projectRoot, params),
28
+ concept_neighbors: (params) => handleConceptNeighbors(projectRoot, params),
29
+ stale_report: (params) => handleStaleReport(projectRoot, params),
30
+ validate_bundle: (params) => handleValidateBundle(projectRoot, params)
31
+ });
32
+
33
+ //#endregion
34
+ export { OkfitToolkit, ToolsLayer };
@@ -0,0 +1,87 @@
1
+ import { ConceptNotFound, InvalidArgument, McpToolError, composeRemediatedMessage, truncateEchoed } from "../errors.js";
2
+ import { loadToolContext } from "../internal/toolContext.js";
3
+ import { toConceptSummary } from "../schema/ConceptSummary.js";
4
+ import { ConceptNeighborsSuccess } from "../schema/tools.js";
5
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
6
+ import { ConceptId, Graph } from "@okfit/core";
7
+ import { Tool } from "effect/unstable/ai";
8
+ import { AppDirs, Xdg } from "@effected/xdg";
9
+
10
+ //#region src/tools/conceptNeighbors.ts
11
+ const DESCRIPTION = "Returns the graph neighbours of one concept: everything it links to (outgoing) and everything that links to it (incoming), each with the node kind and, for a concept target, its full summary. Use it after get_concept to walk the bundle's link graph one hop at a time without loading every concept.";
12
+ /**
13
+ * `dependencies` mirrors the other tools' (Task B1's Deviation 1): without it
14
+ * `Tool.HandlerServices` infers `never`, failing the handler record against
15
+ * `HandlersFrom` when passed to `OkfitToolkit.toLayer`.
16
+ *
17
+ * @public
18
+ */
19
+ const conceptNeighbors = Tool.make("concept_neighbors", {
20
+ description: DESCRIPTION,
21
+ parameters: Schema.Struct({ id: Schema.String }),
22
+ success: ConceptNeighborsSuccess,
23
+ failure: McpToolError,
24
+ dependencies: [
25
+ FileSystem.FileSystem,
26
+ Path.Path,
27
+ AppDirs,
28
+ Xdg
29
+ ]
30
+ }).annotate(Tool.Title, "Concept graph neighbors").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
31
+ /**
32
+ * Deviation from the brief's literal snippet, matching the ruling binding
33
+ * every C task (progress.md): `message` is composed through
34
+ * {@link composeRemediatedMessage} at construction, since a declared typed
35
+ * failure under `failureMode: "error"` never reaches the wire with
36
+ * `structuredContent` — only `error.message` does.
37
+ *
38
+ * @public
39
+ */
40
+ const handleConceptNeighbors = (projectRoot, params) => Effect.gen(function* () {
41
+ const ctx = yield* loadToolContext(projectRoot);
42
+ const normalized = ConceptId.normalize(params.id);
43
+ if (Option.isNone(normalized)) {
44
+ const remediation = {
45
+ hint: "Pass a bundle-relative concept id such as decisions/cli-exit-codes.",
46
+ suggestedTool: "list_concepts"
47
+ };
48
+ return yield* Effect.fail(new InvalidArgument({
49
+ argument: "id",
50
+ message: composeRemediatedMessage("id must not be empty", remediation),
51
+ remediation
52
+ }));
53
+ }
54
+ const id = normalized.value;
55
+ if (!ctx.bundle.concepts.has(id)) {
56
+ const remediation = {
57
+ hint: "Call list_concepts to see the ids this bundle contains.",
58
+ suggestedTool: "list_concepts"
59
+ };
60
+ return yield* Effect.fail(new ConceptNotFound({
61
+ id: params.id,
62
+ message: composeRemediatedMessage(`no concept "${truncateEchoed(params.id)}" in this bundle`, remediation),
63
+ remediation
64
+ }));
65
+ }
66
+ const graph = Graph.fromBundle(ctx.bundle);
67
+ const project = (node) => {
68
+ if (node.kind !== "concept") return {
69
+ id: node.id,
70
+ kind: node.kind,
71
+ summary: null
72
+ };
73
+ const concept = ctx.bundle.concepts.get(node.id);
74
+ return {
75
+ id: node.id,
76
+ kind: node.kind,
77
+ summary: concept === void 0 ? null : toConceptSummary(concept)
78
+ };
79
+ };
80
+ return {
81
+ outgoing: graph.successors(id).map(project),
82
+ incoming: graph.predecessors(id).map(project)
83
+ };
84
+ });
85
+
86
+ //#endregion
87
+ export { conceptNeighbors, handleConceptNeighbors };
@@ -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/cli";
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 };