@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/README.md CHANGED
@@ -2,11 +2,72 @@
2
2
 
3
3
  Model Context Protocol server for [okfit](https://github.com/spencerbeggs/okfit). Gives agents structured access to an Open Knowledge Format bundle: concepts, types, tags, the link graph, and staleness.
4
4
 
5
- > **Part of the okfit kit.** Most users want **[@okfit/plugin](https://www.npmjs.com/package/@okfit/plugin)**, which pulls this package in automatically.
5
+ > **Part of the okfit kit.** Most users want **[@okfit/plugin](https://www.npmjs.com/package/@okfit/plugin)**, which pulls this package in automatically and launches this server through the Claude Code plugin's `mcpServers.mcp` entry.
6
6
 
7
- ## Status
7
+ ## What it is
8
8
 
9
- Skeleton. The `okfit-mcp` bin starts, reports that the server is not implemented, and exits 1.
9
+ `@okfit/mcp` speaks MCP over stdio for one OKF bundle. It is read-only: no
10
+ tool or resource ever writes to the bundle, the config, or anywhere else.
11
+ `validate_bundle` spawns read-only `git log`/`git show` calls for one lint
12
+ (`generated-at-drift`); a read is not a write, and the promise stands. Six
13
+ tools cover orientation, discovery, and validation; the bundle index plus
14
+ one resource per concept expose the bundle's own markdown to a client's
15
+ @-mention UI.
16
+
17
+ ## Launching it
18
+
19
+ Most users never invoke this directly — the Claude Code plugin's
20
+ `bin/start-mcp.sh` loader resolves the project's own
21
+ `node_modules/.bin/okfit-mcp` and falls back to `npx --yes @okfit/mcp` when
22
+ it is not installed. To run it directly, the package's own bin is
23
+ `okfit-mcp`.
24
+
25
+ The server resolves its project root in this order:
26
+ `OKFIT_PROJECT_DIR` → `CLAUDE_PROJECT_DIR` → the process's current working
27
+ directory. No command-line flags are read. `OKFIT_PROJECT_DIR` selects
28
+ where the CLI's own config discovery *starts*, not the project root
29
+ outright — and under the CLI's per-directory resolver order an
30
+ ancestor's `.okfit.toml` never beats a nearer directory's `okfit.toml`
31
+ or `.config/okfit.toml`.
32
+
33
+ ## Tools
34
+
35
+ | Tool | Returns |
36
+ | --- | --- |
37
+ | `describe_vocabulary` | The resolved project and bundle roots, active profile, agent actor, and the config's declared type and tag vocabulary. Call first, before filtering or writing anything. |
38
+ | `list_concepts` | Concept summaries, optionally filtered by an exact type, by tags that must all be present, and by status; pages with `limit`/`offset` and reports the total match count. |
39
+ | `get_concept` | One concept by id: its whole decoded frontmatter, the file's raw markdown text, its bundle-relative path, and every outgoing link. |
40
+ | `concept_neighbors` | A concept's graph neighbours — everything it links to and everything that links to it — each with its node kind and, for a concept target, its full summary. |
41
+ | `stale_report` | Every concept whose `stale_after` instant has passed, each with its summary and how many whole days past it, as of now or an explicit instant. |
42
+ | `validate_bundle` | The same conformance and lint report `okfit validate --format json` produces, unchanged. |
43
+
44
+ ## Resources
45
+
46
+ - `okf://index` — the bundle's root `index.md`, re-read from disk on every
47
+ call.
48
+ - `okf://concept/<id>` — one **static** resource per concept loaded when
49
+ the server starts, one per concept in the bundle at that moment. Editing
50
+ an already-listed concept's file is picked up live on every read; a
51
+ concept added or removed after boot is not reflected in `resources/list`
52
+ until the server restarts. There is no URI template and no completion —
53
+ every concept resource is registered by its own literal URI. The listing
54
+ is one entry per concept and uncursored, sized for bundles of the scale
55
+ okfit targets today.
56
+
57
+ ## Errors
58
+
59
+ A failing tool call reaches the client as `isError: true`, with the
60
+ remediation text folded directly into the message — there is no separate
61
+ structured error field on the wire. The five `McpToolError` members:
62
+
63
+ - `ConfigError` — config discovery, parsing, or validation failed.
64
+ - `BundleNotFound` — the configured bundle root does not exist or could
65
+ not be read.
66
+ - `ConceptNotFound` — no concept in the bundle has the requested id.
67
+ - `UnknownVocabulary` — a requested type or tag name is not declared in
68
+ the resolved config.
69
+ - `InvalidArgument` — a tool argument was structurally acceptable but
70
+ semantically invalid.
10
71
 
11
72
  ## License
12
73
 
package/bin/okfit-mcp.js CHANGED
@@ -1,14 +1,37 @@
1
1
  #!/usr/bin/env node
2
- import { NOT_IMPLEMENTED_EXIT_CODE, NOT_IMPLEMENTED_MESSAGE } from "../index.js";
3
-
4
2
  //#region src/bin.ts
5
3
  /**
6
- * MCP server entry point for okfit. Phase-1 stub.
4
+ * MCP server entry point for okfit.
7
5
  *
8
6
  * @packageDocumentation
9
7
  */
10
- process.stderr.write(`${NOT_IMPLEMENTED_MESSAGE}\n`);
11
- process.exit(1);
8
+ const FATAL_FALLBACK = "okfit-mcp: a fatal error occurred and could not be described.";
9
+ const describe = (error) => {
10
+ try {
11
+ if (error instanceof Error) return error.stack ?? error.message;
12
+ return String(error);
13
+ } catch {
14
+ return FATAL_FALLBACK;
15
+ }
16
+ };
17
+ const fatal = (label, error) => {
18
+ process.stderr.write(`okfit-mcp: ${label}: ${describe(error)}\n`);
19
+ process.exit(1);
20
+ };
21
+ process.on("uncaughtException", (error) => fatal("uncaught exception", error));
22
+ process.on("unhandledRejection", (reason) => fatal("unhandled rejection", reason));
23
+ await (async () => {
24
+ const NodeRuntime = await import("@effect/platform-node/NodeRuntime");
25
+ const NodeServices = await import("@effect/platform-node/NodeServices");
26
+ const { AppDirs, Xdg } = await import("@effected/xdg");
27
+ const { Cause, Exit, Layer, Logger, Runtime } = await import("effect");
28
+ const { resolveMcpProjectRoot } = await import("../internal/projectRoot.js");
29
+ const { ServerLayer } = await import("../server.js");
30
+ const PlatformLayer = Layer.mergeAll(Xdg.layer, AppDirs.layer({ namespace: "okfit" }).pipe(Layer.provide(Xdg.layer))).pipe(Layer.provideMerge(NodeServices.layer));
31
+ const projectRoot = resolveMcpProjectRoot(process.env);
32
+ const program = Layer.launch(ServerLayer(projectRoot).pipe(Layer.provide(PlatformLayer), Layer.provide(Logger.layer([Logger.consolePretty()])), Layer.provide(Layer.succeed(Logger.LogToStderr, true))));
33
+ NodeRuntime.runMain(program, { teardown: (exit, onExit) => Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit) });
34
+ })();
12
35
 
13
36
  //#endregion
14
37
  export { };
package/errors.js ADDED
@@ -0,0 +1,105 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/errors.ts
4
+ /** What a caller should do next about a failed tool call. @public */
5
+ const Remediation = Schema.Struct({
6
+ hint: Schema.String,
7
+ suggestedTool: Schema.optionalKey(Schema.String)
8
+ });
9
+ /**
10
+ * Compose a self-contained wire message from a raw cause message and its
11
+ * remediation: the human message, then the hint, then `Try <suggestedTool>.`
12
+ * when one is present. Every `McpToolError` member's `message` field is
13
+ * built through this at construction — under `failureMode: "error"` (the
14
+ * only mode this contract's tools use), `McpServer`'s own `registerToolkit`
15
+ * collapses a caught typed failure to
16
+ * `{ isError: true, content: [{ type: "text", text: error.message }] }`
17
+ * and never surfaces `structuredContent` for it
18
+ * (`.repos/effect/packages/effect/src/unstable/ai/McpServer.ts:1502-1506,1576-1584`,
19
+ * confirmed empirically in Task B1's own build). `remediation` itself is
20
+ * left on the schema unchanged, both for anything that inspects the typed
21
+ * error directly (a defect handler, a future in-process caller) and
22
+ * because it is what this function reads to build `message`.
23
+ *
24
+ * @public
25
+ */
26
+ const composeRemediatedMessage = (message, remediation) => remediation.suggestedTool === void 0 ? `${message} ${remediation.hint}` : `${message} ${remediation.hint} Try ${remediation.suggestedTool}.`;
27
+ const ECHO_LIMIT = 200;
28
+ /**
29
+ * Truncate a caller-supplied value before it is echoed back inside an
30
+ * error's `message` — the only field a `failureMode: "error"` failure
31
+ * actually delivers to the wire (see {@link composeRemediatedMessage}'s
32
+ * doc comment). Without this, a pathological argument (a multi-megabyte
33
+ * `id`, say) is echoed once in the response's `content[0].text` and once
34
+ * more in the corresponding log line, wasting an agent's context on what
35
+ * is usually a pure typo (final whole-branch review, Minor finding 5).
36
+ *
37
+ * @public
38
+ */
39
+ const truncateEchoed = (value, limit = ECHO_LIMIT) => value.length > limit ? `${value.slice(0, limit)}…` : value;
40
+ /**
41
+ * Config discovery, parsing, or validation failed. `message` is composed
42
+ * through {@link composeRemediatedMessage} at construction, so it is what
43
+ * reaches the wire as `tools/call`'s `content[0].text` (see that
44
+ * function's doc comment for why). @public
45
+ */
46
+ var ConfigError = class extends Schema.TaggedError()("ConfigError", {
47
+ message: Schema.String,
48
+ remediation: Remediation
49
+ }) {};
50
+ /**
51
+ * The configured bundle root does not exist or could not be read.
52
+ * `message` is composed through {@link composeRemediatedMessage} at
53
+ * construction, so it is what reaches the wire as `tools/call`'s
54
+ * `content[0].text`. @public
55
+ */
56
+ var BundleNotFound = class extends Schema.TaggedError()("BundleNotFound", {
57
+ root: Schema.String,
58
+ message: Schema.String,
59
+ remediation: Remediation
60
+ }) {};
61
+ /**
62
+ * No concept in the bundle has the requested id. `message` is composed
63
+ * through {@link composeRemediatedMessage} at construction, so it is what
64
+ * reaches the wire as `tools/call`'s `content[0].text`. @public
65
+ */
66
+ var ConceptNotFound = class extends Schema.TaggedError()("ConceptNotFound", {
67
+ id: Schema.String,
68
+ message: Schema.String,
69
+ remediation: Remediation
70
+ }) {};
71
+ /**
72
+ * A requested type or tag name is not declared in the resolved config.
73
+ * `message` is composed through {@link composeRemediatedMessage} at
74
+ * construction, so it is what reaches the wire as `tools/call`'s
75
+ * `content[0].text`. @public
76
+ */
77
+ var UnknownVocabulary = class extends Schema.TaggedError()("UnknownVocabulary", {
78
+ kind: Schema.Literals(["type", "tag"]),
79
+ requested: Schema.String,
80
+ valid: Schema.Array(Schema.String),
81
+ message: Schema.String,
82
+ remediation: Remediation
83
+ }) {};
84
+ /**
85
+ * A tool argument was structurally acceptable but semantically invalid.
86
+ * `message` is composed through {@link composeRemediatedMessage} at
87
+ * construction, so it is what reaches the wire as `tools/call`'s
88
+ * `content[0].text`. @public
89
+ */
90
+ var InvalidArgument = class extends Schema.TaggedError()("InvalidArgument", {
91
+ argument: Schema.String,
92
+ message: Schema.String,
93
+ remediation: Remediation
94
+ }) {};
95
+ /** The one failure schema every tool declares (N-14). @public */
96
+ const McpToolError = Schema.Union([
97
+ ConfigError,
98
+ BundleNotFound,
99
+ ConceptNotFound,
100
+ UnknownVocabulary,
101
+ InvalidArgument
102
+ ]);
103
+
104
+ //#endregion
105
+ export { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary, composeRemediatedMessage, truncateEchoed };