@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 +64 -3
- package/bin/okfit-mcp.js +28 -5
- 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/package.js +5 -0
- package/package.json +8 -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 +13 -0
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
|
-
##
|
|
7
|
+
## What it is
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
4
|
+
* MCP server entry point for okfit.
|
|
7
5
|
*
|
|
8
6
|
* @packageDocumentation
|
|
9
7
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
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 };
|