@okfit/mcp 0.5.3 → 0.6.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/errors.js CHANGED
@@ -1,100 +1,43 @@
1
+ import { Remediation } from "@effected/engine";
2
+ import { ToolFailure } from "@effected/mcp";
1
3
  import { Schema } from "effect";
2
4
 
3
5
  //#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
6
  /**
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:1774-1778,1842-1846`
19
- * at `effect@4.0.0-rc.116`: `toolErrorResult` and `declaredFailureResult`'s
20
- * `error instanceof Error` branch; confirmed empirically in Task B1's own
21
- * build). Since rc.116 that declared branch is rendered WITHOUT a log line
22
- * — only an internal failure goes through `Effect.logError` — so the
23
- * message text is the whole of what a client ever sees. `remediation` itself is
24
- * left on the schema unchanged, both for anything that inspects the typed
25
- * error directly (a defect handler, a future in-process caller) and
26
- * because it is what this function reads to build `message`.
27
- *
28
- * @public
7
+ * Config discovery, parsing, or validation failed. @public
29
8
  */
30
- const composeRemediatedMessage = (message, remediation) => remediation.suggestedTool === void 0 ? `${message} ${remediation.hint}` : `${message} ${remediation.hint} Try ${remediation.suggestedTool}.`;
31
- const ECHO_LIMIT = 200;
9
+ var ConfigError = class extends Schema.TaggedError()("ConfigError", { ...ToolFailure.fields }) {};
32
10
  /**
33
- * Truncate a caller-supplied value before it is echoed back inside an
34
- * error's `message` — the only field a `failureMode: "error"` failure
35
- * actually delivers to the wire (see {@link composeRemediatedMessage}'s
36
- * doc comment). Without this, a pathological argument (a multi-megabyte
37
- * `id`, say) is echoed once in the response's `content[0].text` and once
38
- * more in the corresponding log line, wasting an agent's context on what
39
- * is usually a pure typo (final whole-branch review, Minor finding 5).
40
- *
41
- * @public
42
- */
43
- const truncateEchoed = (value, limit = ECHO_LIMIT) => value.length > limit ? `${value.slice(0, limit)}…` : value;
44
- /**
45
- * Config discovery, parsing, or validation failed. `message` is composed
46
- * through {@link composeRemediatedMessage} at construction, so it is what
47
- * reaches the wire as `tools/call`'s `content[0].text` (see that
48
- * function's doc comment for why). @public
49
- */
50
- var ConfigError = class extends Schema.TaggedError()("ConfigError", {
51
- message: Schema.String,
52
- remediation: Remediation
53
- }) {};
54
- /**
55
- * The configured bundle root does not exist or could not be read.
56
- * `message` is composed through {@link composeRemediatedMessage} at
57
- * construction, so it is what reaches the wire as `tools/call`'s
58
- * `content[0].text`. @public
11
+ * The configured bundle root does not exist or could not be read. @public
59
12
  */
60
13
  var BundleNotFound = class extends Schema.TaggedError()("BundleNotFound", {
61
- root: Schema.String,
62
- message: Schema.String,
63
- remediation: Remediation
14
+ ...ToolFailure.fields,
15
+ root: Schema.String
64
16
  }) {};
65
17
  /**
66
- * No concept in the bundle has the requested id. `message` is composed
67
- * through {@link composeRemediatedMessage} at construction, so it is what
68
- * reaches the wire as `tools/call`'s `content[0].text`. @public
18
+ * No concept in the bundle has the requested id. @public
69
19
  */
70
20
  var ConceptNotFound = class extends Schema.TaggedError()("ConceptNotFound", {
71
- id: Schema.String,
72
- message: Schema.String,
73
- remediation: Remediation
21
+ ...ToolFailure.fields,
22
+ id: Schema.String
74
23
  }) {};
75
24
  /**
76
25
  * A requested type or tag name is not declared in the resolved config.
77
- * `message` is composed through {@link composeRemediatedMessage} at
78
- * construction, so it is what reaches the wire as `tools/call`'s
79
- * `content[0].text`. @public
26
+ * @public
80
27
  */
81
28
  var UnknownVocabulary = class extends Schema.TaggedError()("UnknownVocabulary", {
29
+ ...ToolFailure.fields,
82
30
  kind: Schema.Literals(["type", "tag"]),
83
31
  requested: Schema.String,
84
- valid: Schema.Array(Schema.String),
85
- message: Schema.String,
86
- remediation: Remediation
32
+ valid: Schema.Array(Schema.String)
87
33
  }) {};
88
34
  /**
89
35
  * A tool argument was structurally acceptable but semantically invalid.
90
- * `message` is composed through {@link composeRemediatedMessage} at
91
- * construction, so it is what reaches the wire as `tools/call`'s
92
- * `content[0].text`. @public
36
+ * @public
93
37
  */
94
38
  var InvalidArgument = class extends Schema.TaggedError()("InvalidArgument", {
95
- argument: Schema.String,
96
- message: Schema.String,
97
- remediation: Remediation
39
+ ...ToolFailure.fields,
40
+ argument: Schema.String
98
41
  }) {};
99
42
  /** The one failure schema every tool declares (N-14). @public */
100
43
  const McpToolError = Schema.Union([
@@ -106,4 +49,4 @@ const McpToolError = Schema.Union([
106
49
  ]);
107
50
 
108
51
  //#endregion
109
- export { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary, composeRemediatedMessage, truncateEchoed };
52
+ export { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary };
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Remediation } from "@effected/engine";
1
2
  import { FileSystem, Layer, Path, Schema, Stdio } from "effect";
2
3
  import { AppDirs, Xdg } from "@effected/xdg";
3
4
  import { LoadedConcept } from "@okfit/core";
@@ -5,120 +6,72 @@ import { Distribution } from "@okfit/engine";
5
6
  import { ChildProcessSpawner } from "effect/unstable/process";
6
7
  import { Toolkit } from "effect/unstable/ai";
7
8
  //#region src/errors.d.ts
8
- /** What a caller should do next about a failed tool call. @public */
9
- export declare const Remediation: Schema.Struct<{
10
- readonly hint: Schema.String;
11
- readonly suggestedTool: Schema.optionalKey<Schema.String>;
12
- }>;
13
- /** @public */
14
- export type Remediation = typeof Remediation.Type;
15
- /**
16
- * Compose a self-contained wire message from a raw cause message and its
17
- * remediation: the human message, then the hint, then `Try <suggestedTool>.`
18
- * when one is present. Every `McpToolError` member's `message` field is
19
- * built through this at construction — under `failureMode: "error"` (the
20
- * only mode this contract's tools use), `McpServer`'s own `registerToolkit`
21
- * collapses a caught typed failure to
22
- * `{ isError: true, content: [{ type: "text", text: error.message }] }`
23
- * and never surfaces `structuredContent` for it
24
- * (`.repos/effect/packages/effect/src/unstable/ai/McpServer.ts:1774-1778,1842-1846`
25
- * at `effect@4.0.0-rc.116`: `toolErrorResult` and `declaredFailureResult`'s
26
- * `error instanceof Error` branch; confirmed empirically in Task B1's own
27
- * build). Since rc.116 that declared branch is rendered WITHOUT a log line
28
- * — only an internal failure goes through `Effect.logError` — so the
29
- * message text is the whole of what a client ever sees. `remediation` itself is
30
- * left on the schema unchanged, both for anything that inspects the typed
31
- * error directly (a defect handler, a future in-process caller) and
32
- * because it is what this function reads to build `message`.
33
- *
34
- * @public
35
- */
36
- export declare const composeRemediatedMessage: (message: string, remediation: Remediation) => string;
37
- /**
38
- * Truncate a caller-supplied value before it is echoed back inside an
39
- * error's `message` — the only field a `failureMode: "error"` failure
40
- * actually delivers to the wire (see {@link composeRemediatedMessage}'s
41
- * doc comment). Without this, a pathological argument (a multi-megabyte
42
- * `id`, say) is echoed once in the response's `content[0].text` and once
43
- * more in the corresponding log line, wasting an agent's context on what
44
- * is usually a pure typo (final whole-branch review, Minor finding 5).
45
- *
46
- * @public
47
- */
48
- export declare const truncateEchoed: (value: string, limit?: number) => string;
49
9
  declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
50
10
  readonly message: Schema.String;
51
11
  readonly remediation: Schema.Struct<{
52
12
  readonly hint: Schema.String;
53
13
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
14
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
54
15
  }>;
55
16
  }>, import("effect/Cause").YieldableError>;
56
17
  /**
57
- * Config discovery, parsing, or validation failed. `message` is composed
58
- * through {@link composeRemediatedMessage} at construction, so it is what
59
- * reaches the wire as `tools/call`'s `content[0].text` (see that
60
- * function's doc comment for why). @public
18
+ * Config discovery, parsing, or validation failed. @public
61
19
  */
62
20
  export declare class ConfigError extends ConfigError_base {}
63
21
  declare const BundleNotFound_base: Schema.Class<BundleNotFound, Schema.TaggedStruct<"BundleNotFound", {
64
- readonly root: Schema.String;
65
22
  readonly message: Schema.String;
66
23
  readonly remediation: Schema.Struct<{
67
24
  readonly hint: Schema.String;
68
25
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
26
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
69
27
  }>;
28
+ readonly root: Schema.String;
70
29
  }>, import("effect/Cause").YieldableError>;
71
30
  /**
72
- * The configured bundle root does not exist or could not be read.
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
31
+ * The configured bundle root does not exist or could not be read. @public
76
32
  */
77
33
  export declare class BundleNotFound extends BundleNotFound_base {}
78
34
  declare const ConceptNotFound_base: Schema.Class<ConceptNotFound, Schema.TaggedStruct<"ConceptNotFound", {
79
- readonly id: Schema.String;
80
35
  readonly message: Schema.String;
81
36
  readonly remediation: Schema.Struct<{
82
37
  readonly hint: Schema.String;
83
38
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
39
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
84
40
  }>;
41
+ readonly id: Schema.String;
85
42
  }>, import("effect/Cause").YieldableError>;
86
43
  /**
87
- * No concept in the bundle has the requested id. `message` is composed
88
- * through {@link composeRemediatedMessage} at construction, so it is what
89
- * reaches the wire as `tools/call`'s `content[0].text`. @public
44
+ * No concept in the bundle has the requested id. @public
90
45
  */
91
46
  export declare class ConceptNotFound extends ConceptNotFound_base {}
92
47
  declare const UnknownVocabulary_base: Schema.Class<UnknownVocabulary, Schema.TaggedStruct<"UnknownVocabulary", {
93
- readonly kind: Schema.Literals<readonly ["type", "tag"]>;
94
- readonly requested: Schema.String;
95
- readonly valid: Schema.$Array<Schema.String>;
96
48
  readonly message: Schema.String;
97
49
  readonly remediation: Schema.Struct<{
98
50
  readonly hint: Schema.String;
99
51
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
52
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
100
53
  }>;
54
+ readonly kind: Schema.Literals<readonly ["type", "tag"]>;
55
+ readonly requested: Schema.String;
56
+ readonly valid: Schema.$Array<Schema.String>;
101
57
  }>, import("effect/Cause").YieldableError>;
102
58
  /**
103
59
  * A requested type or tag name is not declared in the resolved config.
104
- * `message` is composed through {@link composeRemediatedMessage} at
105
- * construction, so it is what reaches the wire as `tools/call`'s
106
- * `content[0].text`. @public
60
+ * @public
107
61
  */
108
62
  export declare class UnknownVocabulary extends UnknownVocabulary_base {}
109
63
  declare const InvalidArgument_base: Schema.Class<InvalidArgument, Schema.TaggedStruct<"InvalidArgument", {
110
- readonly argument: Schema.String;
111
64
  readonly message: Schema.String;
112
65
  readonly remediation: Schema.Struct<{
113
66
  readonly hint: Schema.String;
114
67
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
68
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
115
69
  }>;
70
+ readonly argument: Schema.String;
116
71
  }>, import("effect/Cause").YieldableError>;
117
72
  /**
118
73
  * A tool argument was structurally acceptable but semantically invalid.
119
- * `message` is composed through {@link composeRemediatedMessage} at
120
- * construction, so it is what reaches the wire as `tools/call`'s
121
- * `content[0].text`. @public
74
+ * @public
122
75
  */
123
76
  export declare class InvalidArgument extends InvalidArgument_base {}
124
77
  /** The one failure schema every tool declares (N-14). @public */
@@ -371,9 +324,8 @@ export type ValidateBundleParams = typeof ValidateBundleParams.Type;
371
324
  /**
372
325
  * Everything `ServerLayer` still needs from the platform: the four services
373
326
  * `describe_vocabulary`'s declared `Tool.make` dependencies pull through
374
- * `McpServer.toolkit`'s own `Tool.HandlerServices<Tools>` requirement, plus
375
- * `Stdio`, which `McpServer.layerStdio` itself requires
376
- * (`unstable/ai/McpServer.ts:1207-1216`) and which the brief's own
327
+ * `McpToolkit.layer`'s own `Tool.HandlerServices<Tools>` requirement, plus
328
+ * `Stdio`, which `McpStdio.layer` itself requires and which the brief's own
377
329
  * `PlatformServices` literal omitted.
378
330
  *
379
331
  * `ChildProcessSpawner` is new (contract §10.3, S-16): `validate_bundle`'s
@@ -415,24 +367,34 @@ export declare const SERVER_INSTRUCTIONS: string;
415
367
  * The whole server as one layer: the toolkit, one static resource per
416
368
  * concept (`okf://concept/<id>`, built once at boot — see
417
369
  * {@link ConceptResources}), and `okf://index` (re-read from disk on every
418
- * call), over `McpServer.layerStdio`.
370
+ * call), over `@effected/mcp`'s `McpStdio.layer`.
419
371
  *
420
- * `protocols` ships three adapters, newest first. `McpProtocol.v2026_07_28`
421
- * is the stateless adapter (SEP-2575): no `initialize`, no session, every
422
- * request self-identifies through `params._meta`, and the client discovers
423
- * the server with `server/discover`. The two stateful adapters stay
424
- * because `initialize` matches stateful adapters only — a client that
425
- * opens with `initialize` (Claude Code by default, Copilot, Cursor, the
426
- * Inspector) would otherwise get `METHOD_NOT_FOUND`. Array order is
427
- * load-bearing: a request with no session and no `_meta` protocol version
428
- * falls to `protocols[0]`, and `server/discover` advertises every listed
429
- * adapter in `supportedVersions`. The runtime allows at most one stateless
430
- * adapter; a second fails the layer with `Cause.IllegalArgumentError`.
431
- * Never reduce this to one entry.
372
+ * `McpStdio.layer` (not core's own `McpServer.layerStdio`, hand-wired) is
373
+ * load-bearing, not a style choice: it wraps core's stdin decoder with a
374
+ * guard that answers a non-JSON line with a JSON-RPC `-32700` and keeps
375
+ * serving, where hand-wiring `layerStdio` directly wedges permanently on
376
+ * the first bad line (effect-v4-mcp's `server-wiring.md#stdin-guard`). It
377
+ * also merges `LogToStderr` into everything it provides, so `main.ts` no
378
+ * longer assembles a logger of its own. `McpToolkit.layer` (not core's
379
+ * `McpServer.toolkit`) reports every unknown argument key a strict tool
380
+ * call carries, at every depth, in one response, instead of only the
381
+ * first.
432
382
  *
433
- * `Cause.IllegalArgumentError` in `layerStdio`'s signature is left
434
- * unhandled: `protocols` is a static literal, so it is an implementer-time
435
- * defect, not a runtime condition.
383
+ * `protocols` ships three adapters, newest first -- the same list
384
+ * `McpStdio.protocols` defaults to, spelled out here since the decision
385
+ * behind the exact order is this package's own
386
+ * (`okf/decisions/mcp-stateless-first-protocol-list.md`), not the kit's.
387
+ * `McpProtocol.v2026_07_28` is the stateless adapter (SEP-2575): no
388
+ * `initialize`, no session, every request self-identifies through
389
+ * `params._meta`, and the client discovers the server with
390
+ * `server/discover`. The two stateful adapters stay because `initialize`
391
+ * matches stateful adapters only — a client that opens with `initialize`
392
+ * (Claude Code by default, Copilot, Cursor, the Inspector) would otherwise
393
+ * get `METHOD_NOT_FOUND`. Array order is load-bearing: a request with no
394
+ * session and no `_meta` protocol version falls to `protocols[0]`, and
395
+ * `server/discover` advertises every listed adapter in `supportedVersions`.
396
+ * The runtime allows at most one stateless adapter; a second fails the
397
+ * layer. Never reduce this to one entry.
436
398
  *
437
399
  * @public
438
400
  */
@@ -827,5 +789,5 @@ export declare const ToolsLayer: (projectRoot: string, distribution?: Distributi
827
789
  */
828
790
  export declare const MCP_VERSION: string;
829
791
  //#endregion
830
- export type { PlatformServices, ServerOptions };
792
+ export { type PlatformServices, Remediation, type ServerOptions };
831
793
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary, composeRemediatedMessage, truncateEchoed } from "./errors.js";
1
+ import { BundleNotFound, ConceptNotFound, ConfigError, InvalidArgument, McpToolError, Remediation, UnknownVocabulary } from "./errors.js";
2
2
  import { ConceptResources } from "./resources/conceptResource.js";
3
3
  import { IndexResource } from "./resources/indexResource.js";
4
4
  import { ConceptSummary, toConceptSummary } from "./schema/ConceptSummary.js";
@@ -7,4 +7,4 @@ import { MCP_VERSION } from "./version.js";
7
7
  import { OkfitToolkit, ToolsLayer } from "./toolkit.js";
8
8
  import { SERVER_INSTRUCTIONS, ServerLayer } from "./server.js";
9
9
 
10
- export { BundleNotFound, ConceptNeighborsSuccess, ConceptNotFound, ConceptResources, ConceptSummary, ConfigError, DescribeVocabularySuccess, GetConceptSuccess, IndexResource, InvalidArgument, ListConceptsParams, ListConceptsSuccess, MCP_VERSION, McpToolError, Neighbor, OkfitToolkit, Remediation, SERVER_INSTRUCTIONS, ServerLayer, StaleReportParams, StaleReportSuccess, ToolsLayer, UnknownVocabulary, ValidateBundleParams, composeRemediatedMessage, toConceptSummary, truncateEchoed };
10
+ export { BundleNotFound, ConceptNeighborsSuccess, ConceptNotFound, ConceptResources, ConceptSummary, ConfigError, DescribeVocabularySuccess, GetConceptSuccess, IndexResource, InvalidArgument, ListConceptsParams, ListConceptsSuccess, MCP_VERSION, McpToolError, Neighbor, OkfitToolkit, Remediation, SERVER_INSTRUCTIONS, ServerLayer, StaleReportParams, StaleReportSuccess, ToolsLayer, UnknownVocabulary, ValidateBundleParams, toConceptSummary };
@@ -1,14 +1,29 @@
1
+ import { LaunchContext } from "@effected/engine";
2
+
1
3
  //#region src/internal/projectRoot.ts
2
4
  /**
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.
5
+ * N-21's precedence: `OKFIT_PROJECT_DIR`, else `CLAUDE_PROJECT_DIR`, else
6
+ * `cwd`. Pure over its `env`/`cwd` arguments so tests never touch
7
+ * `process.env`/`process.cwd()` -- both are read once, in `main.ts`, and
8
+ * passed down as plain values (`main.ts` is the only place this package
9
+ * reads `process`).
10
+ *
11
+ * Built on `@effected/engine`'s `LaunchContext.projectDir` with an empty
12
+ * `argv`: this server has no command parser and `start-mcp.sh` forwards no
13
+ * positional arguments, so the only candidates are the two env keys and the
14
+ * cwd fallback. `LaunchContext.projectDir` additionally treats a value
15
+ * still carrying a literal `${VAR}` placeholder as unusable -- protection
16
+ * the old hand-rolled `??` chain did not have -- against an agent host that
17
+ * leaves `CLAUDE_PROJECT_DIR` unexpanded in some launch paths.
8
18
  *
9
19
  * @internal
10
20
  */
11
- const resolveMcpProjectRoot = (env) => env.OKFIT_PROJECT_DIR ?? env.CLAUDE_PROJECT_DIR ?? process.cwd();
21
+ const resolveMcpProjectRoot = (env, cwd) => LaunchContext.projectDir({
22
+ argv: [],
23
+ env,
24
+ keys: ["OKFIT_PROJECT_DIR", "CLAUDE_PROJECT_DIR"],
25
+ cwd
26
+ });
12
27
 
13
28
  //#endregion
14
29
  export { resolveMcpProjectRoot };
@@ -1,4 +1,5 @@
1
- import { InvalidArgument, composeRemediatedMessage } from "../errors.js";
1
+ import { InvalidArgument } from "../errors.js";
2
+ import { ToolFailure } from "@effected/mcp";
2
3
  import { DateTime, Effect, Schema } from "effect";
3
4
  import { Timestamp } from "@okfit/core";
4
5
 
@@ -17,7 +18,7 @@ const resolveNow = (input) => input === void 0 ? DateTime.now : Schema.decodeUnk
17
18
  const remediation = { hint: "now must be an ISO-8601 instant with an explicit offset, for example 2026-09-06T00:00:00Z." };
18
19
  return new InvalidArgument({
19
20
  argument: "now",
20
- message: composeRemediatedMessage(String(issue), remediation),
21
+ message: ToolFailure.message(String(issue), remediation),
21
22
  remediation
22
23
  });
23
24
  }));
@@ -1,4 +1,5 @@
1
- import { BundleNotFound, ConfigError, composeRemediatedMessage } from "../errors.js";
1
+ import { BundleNotFound, ConfigError } from "../errors.js";
2
+ import { ToolFailure } from "@effected/mcp";
2
3
  import { Effect, Option } from "effect";
3
4
  import { Bundle } from "@okfit/core";
4
5
  import { provideConfig, resolveProjectConfig } from "@okfit/engine";
@@ -43,7 +44,7 @@ const resolveConfigOnly = (projectRoot) => resolveProjectConfig({
43
44
  suggestedTool: "describe_vocabulary"
44
45
  };
45
46
  return new ConfigError({
46
- message: composeRemediatedMessage(messageOf(cause), remediation),
47
+ message: ToolFailure.message(messageOf(cause), remediation),
47
48
  remediation
48
49
  });
49
50
  }));
@@ -60,7 +61,7 @@ const loadToolContext = (projectRoot) => Effect.gen(function* () {
60
61
  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
62
  return new BundleNotFound({
62
63
  root: resolved.bundleRoot,
63
- message: composeRemediatedMessage(cause.message, remediation),
64
+ message: ToolFailure.message(cause.message, remediation),
64
65
  remediation
65
66
  });
66
67
  }));
package/main.d.ts CHANGED
@@ -16,11 +16,23 @@ export interface MainOptions {
16
16
  *
17
17
  * This module deliberately carries NO static imports of the server graph:
18
18
  * the `uncaughtException` and `unhandledRejection` handlers are registered
19
- * before `NodeRuntime`, the logger and `ServerLayer` are ever evaluated, so
20
- * a throw during module evaluation is still reported on stderr rather than
21
- * crashing silently. Adding a static import here would defeat that --
22
- * `Distribution` above is a type-only import, so it carries no runtime
23
- * import at all.
19
+ * before `NodeRuntime`, `@effected/mcp` and `ServerLayer` are ever
20
+ * evaluated, so a throw during module evaluation is still reported on
21
+ * stderr rather than crashing silently. Adding a static import here would
22
+ * defeat that -- `Distribution` above is a type-only import, so it carries
23
+ * no runtime import at all. `@effected/mcp`'s `McpStdio.launch`/`teardown`
24
+ * sits AROUND this requirement, not in place of it (design-patterns'
25
+ * `carrier-entry-contract.md`).
26
+ *
27
+ * Assembled with `McpStdio.launch`/`McpStdio.teardown` (effect-v4-mcp's
28
+ * `server-wiring.md`): `launch` reports a launch failure itself, on
29
+ * stderr, before `NodeRuntime.runMain`'s own out-of-scope report could
30
+ * print it to stdout -- the JSON-RPC wire; `teardown` maps stdin EOF (a
31
+ * normal client disconnect) to exit `0` instead of the default `130`. Both
32
+ * replace this module's own hand-rolled equivalents. `ServerLayer`'s stdio
33
+ * server (`server.ts`) is built on `McpStdio.layer`, which already merges
34
+ * `LogToStderr` into everything it provides, so this module no longer
35
+ * assembles a logger itself either.
24
36
  *
25
37
  * @public
26
38
  */
package/main.js CHANGED
@@ -17,25 +17,37 @@ const fatal = (label, error) => {
17
17
  *
18
18
  * This module deliberately carries NO static imports of the server graph:
19
19
  * the `uncaughtException` and `unhandledRejection` handlers are registered
20
- * before `NodeRuntime`, the logger and `ServerLayer` are ever evaluated, so
21
- * a throw during module evaluation is still reported on stderr rather than
22
- * crashing silently. Adding a static import here would defeat that --
23
- * `Distribution` above is a type-only import, so it carries no runtime
24
- * import at all.
20
+ * before `NodeRuntime`, `@effected/mcp` and `ServerLayer` are ever
21
+ * evaluated, so a throw during module evaluation is still reported on
22
+ * stderr rather than crashing silently. Adding a static import here would
23
+ * defeat that -- `Distribution` above is a type-only import, so it carries
24
+ * no runtime import at all. `@effected/mcp`'s `McpStdio.launch`/`teardown`
25
+ * sits AROUND this requirement, not in place of it (design-patterns'
26
+ * `carrier-entry-contract.md`).
27
+ *
28
+ * Assembled with `McpStdio.launch`/`McpStdio.teardown` (effect-v4-mcp's
29
+ * `server-wiring.md`): `launch` reports a launch failure itself, on
30
+ * stderr, before `NodeRuntime.runMain`'s own out-of-scope report could
31
+ * print it to stdout -- the JSON-RPC wire; `teardown` maps stdin EOF (a
32
+ * normal client disconnect) to exit `0` instead of the default `130`. Both
33
+ * replace this module's own hand-rolled equivalents. `ServerLayer`'s stdio
34
+ * server (`server.ts`) is built on `McpStdio.layer`, which already merges
35
+ * `LogToStderr` into everything it provides, so this module no longer
36
+ * assembles a logger itself either.
25
37
  *
26
38
  * @public
27
39
  */
28
40
  const main = async (options = {}) => {
29
41
  process.on("uncaughtException", (error) => fatal("uncaught exception", error));
30
42
  process.on("unhandledRejection", (reason) => fatal("unhandled rejection", reason));
43
+ const { McpStdio } = await import("@effected/mcp");
31
44
  const NodeRuntime = await import("@effect/platform-node/NodeRuntime");
32
45
  const { OkfitPlatform } = await import("@okfit/engine");
33
- const { Cause, Exit, Layer, Logger, Runtime } = await import("effect");
46
+ const { Layer } = await import("effect");
34
47
  const { resolveMcpProjectRoot } = await import("./internal/projectRoot.js");
35
48
  const { ServerLayer } = await import("./server.js");
36
- const projectRoot = resolveMcpProjectRoot(process.env);
37
- const program = Layer.launch(ServerLayer(projectRoot, options).pipe(Layer.provide(OkfitPlatform), Layer.provide(Logger.layer([Logger.consolePretty()])), Layer.provide(Layer.succeed(Logger.LogToStderr, true))));
38
- NodeRuntime.runMain(program, { teardown: (exit, onExit) => Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit) });
49
+ const Main = ServerLayer(resolveMcpProjectRoot(process.env, process.cwd()), options).pipe(Layer.provide(OkfitPlatform));
50
+ NodeRuntime.runMain(McpStdio.launch(Main), { teardown: McpStdio.teardown });
39
51
  };
40
52
 
41
53
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/mcp",
3
- "version": "0.5.3",
3
+ "version": "0.6.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": [
@@ -45,10 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@effect/platform-node": "4.0.0-rc.117",
48
+ "@effected/engine": "^0.1.0",
48
49
  "@effected/git": "^0.17.0",
50
+ "@effected/mcp": "^0.1.1",
49
51
  "@effected/xdg": "^0.7.0",
50
52
  "@okfit/core": "0.8.0",
51
- "@okfit/engine": "0.8.0",
53
+ "@okfit/engine": "0.9.0",
52
54
  "@okfit/profiles": "0.8.0",
53
55
  "effect": "4.0.0-rc.117"
54
56
  },
package/server.js CHANGED
@@ -2,8 +2,9 @@ import { ConceptResources } from "./resources/conceptResource.js";
2
2
  import { IndexResource } from "./resources/indexResource.js";
3
3
  import { MCP_VERSION } from "./version.js";
4
4
  import { OkfitToolkit, ToolsLayer } from "./toolkit.js";
5
+ import { McpStdio, McpToolkit } from "@effected/mcp";
5
6
  import { Layer } from "effect";
6
- import { McpProtocol, McpServer } from "effect/unstable/ai";
7
+ import { McpProtocol } from "effect/unstable/ai";
7
8
  import { Git } from "@effected/git";
8
9
  import { GitHistory } from "@okfit/profiles";
9
10
 
@@ -33,28 +34,38 @@ const SERVER_INSTRUCTIONS = [
33
34
  * The whole server as one layer: the toolkit, one static resource per
34
35
  * concept (`okf://concept/<id>`, built once at boot — see
35
36
  * {@link ConceptResources}), and `okf://index` (re-read from disk on every
36
- * call), over `McpServer.layerStdio`.
37
+ * call), over `@effected/mcp`'s `McpStdio.layer`.
37
38
  *
38
- * `protocols` ships three adapters, newest first. `McpProtocol.v2026_07_28`
39
- * is the stateless adapter (SEP-2575): no `initialize`, no session, every
40
- * request self-identifies through `params._meta`, and the client discovers
41
- * the server with `server/discover`. The two stateful adapters stay
42
- * because `initialize` matches stateful adapters only — a client that
43
- * opens with `initialize` (Claude Code by default, Copilot, Cursor, the
44
- * Inspector) would otherwise get `METHOD_NOT_FOUND`. Array order is
45
- * load-bearing: a request with no session and no `_meta` protocol version
46
- * falls to `protocols[0]`, and `server/discover` advertises every listed
47
- * adapter in `supportedVersions`. The runtime allows at most one stateless
48
- * adapter; a second fails the layer with `Cause.IllegalArgumentError`.
49
- * Never reduce this to one entry.
39
+ * `McpStdio.layer` (not core's own `McpServer.layerStdio`, hand-wired) is
40
+ * load-bearing, not a style choice: it wraps core's stdin decoder with a
41
+ * guard that answers a non-JSON line with a JSON-RPC `-32700` and keeps
42
+ * serving, where hand-wiring `layerStdio` directly wedges permanently on
43
+ * the first bad line (effect-v4-mcp's `server-wiring.md#stdin-guard`). It
44
+ * also merges `LogToStderr` into everything it provides, so `main.ts` no
45
+ * longer assembles a logger of its own. `McpToolkit.layer` (not core's
46
+ * `McpServer.toolkit`) reports every unknown argument key a strict tool
47
+ * call carries, at every depth, in one response, instead of only the
48
+ * first.
50
49
  *
51
- * `Cause.IllegalArgumentError` in `layerStdio`'s signature is left
52
- * unhandled: `protocols` is a static literal, so it is an implementer-time
53
- * defect, not a runtime condition.
50
+ * `protocols` ships three adapters, newest first -- the same list
51
+ * `McpStdio.protocols` defaults to, spelled out here since the decision
52
+ * behind the exact order is this package's own
53
+ * (`okf/decisions/mcp-stateless-first-protocol-list.md`), not the kit's.
54
+ * `McpProtocol.v2026_07_28` is the stateless adapter (SEP-2575): no
55
+ * `initialize`, no session, every request self-identifies through
56
+ * `params._meta`, and the client discovers the server with
57
+ * `server/discover`. The two stateful adapters stay because `initialize`
58
+ * matches stateful adapters only — a client that opens with `initialize`
59
+ * (Claude Code by default, Copilot, Cursor, the Inspector) would otherwise
60
+ * get `METHOD_NOT_FOUND`. Array order is load-bearing: a request with no
61
+ * session and no `_meta` protocol version falls to `protocols[0]`, and
62
+ * `server/discover` advertises every listed adapter in `supportedVersions`.
63
+ * The runtime allows at most one stateless adapter; a second fails the
64
+ * layer. Never reduce this to one entry.
54
65
  *
55
66
  * @public
56
67
  */
57
- const ServerLayer = (projectRoot, options = {}) => Layer.mergeAll(McpServer.toolkit(OkfitToolkit).pipe(Layer.provideMerge(ToolsLayer(projectRoot, options.distribution))), ConceptResources(projectRoot), IndexResource(projectRoot)).pipe(Layer.provide(Layer.mergeAll(Git.layer, GitHistory.layer)), Layer.provide(McpServer.layerStdio({
68
+ const ServerLayer = (projectRoot, options = {}) => Layer.mergeAll(McpToolkit.layer(OkfitToolkit).pipe(Layer.provideMerge(ToolsLayer(projectRoot, options.distribution))), ConceptResources(projectRoot), IndexResource(projectRoot)).pipe(Layer.provide(Layer.mergeAll(Git.layer, GitHistory.layer)), Layer.provide(McpStdio.layer({
58
69
  name: "okfit",
59
70
  version: MCP_VERSION,
60
71
  instructions: SERVER_INSTRUCTIONS,
@@ -63,7 +74,7 @@ const ServerLayer = (projectRoot, options = {}) => Layer.mergeAll(McpServer.tool
63
74
  McpProtocol.v2025_11_25,
64
75
  McpProtocol.v2025_06_18
65
76
  ]
66
- })), Layer.orDie);
77
+ })));
67
78
 
68
79
  //#endregion
69
80
  export { SERVER_INSTRUCTIONS, ServerLayer };
@@ -1,7 +1,8 @@
1
- import { ConceptNotFound, InvalidArgument, McpToolError, composeRemediatedMessage, truncateEchoed } from "../errors.js";
1
+ import { ConceptNotFound, InvalidArgument, McpToolError } from "../errors.js";
2
2
  import { loadToolContext } from "../internal/toolContext.js";
3
3
  import { toConceptSummary } from "../schema/ConceptSummary.js";
4
4
  import { ConceptNeighborsSuccess } from "../schema/tools.js";
5
+ import { ToolFailure } from "@effected/mcp";
5
6
  import { Effect, FileSystem, Option, Path, Schema } from "effect";
6
7
  import { ConceptId, Graph } from "@okfit/core";
7
8
  import { Tool } from "effect/unstable/ai";
@@ -31,8 +32,8 @@ const conceptNeighbors = Tool.make("concept_neighbors", {
31
32
  /**
32
33
  * Deviation from the brief's literal snippet, matching the ruling binding
33
34
  * 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
35
+ * `ToolFailure.message` (`@effected/mcp`) at construction, since a declared
36
+ * typed failure under `failureMode: "error"` never reaches the wire with
36
37
  * `structuredContent` — only `error.message` does.
37
38
  *
38
39
  * @public
@@ -47,7 +48,7 @@ const handleConceptNeighbors = (projectRoot, params) => Effect.gen(function* ()
47
48
  };
48
49
  return yield* Effect.fail(new InvalidArgument({
49
50
  argument: "id",
50
- message: composeRemediatedMessage("id must not be empty", remediation),
51
+ message: ToolFailure.message("id must not be empty", remediation),
51
52
  remediation
52
53
  }));
53
54
  }
@@ -59,7 +60,7 @@ const handleConceptNeighbors = (projectRoot, params) => Effect.gen(function* ()
59
60
  };
60
61
  return yield* Effect.fail(new ConceptNotFound({
61
62
  id: params.id,
62
- message: composeRemediatedMessage(`no concept "${truncateEchoed(params.id)}" in this bundle`, remediation),
63
+ message: ToolFailure.message(`no concept "${ToolFailure.truncate(params.id)}" in this bundle`, remediation),
63
64
  remediation
64
65
  }));
65
66
  }
@@ -1,6 +1,7 @@
1
- import { ConceptNotFound, InvalidArgument, McpToolError, composeRemediatedMessage, truncateEchoed } from "../errors.js";
1
+ import { ConceptNotFound, InvalidArgument, McpToolError } from "../errors.js";
2
2
  import { loadToolContext } from "../internal/toolContext.js";
3
3
  import { GetConceptSuccess } from "../schema/tools.js";
4
+ import { ToolFailure } from "@effected/mcp";
4
5
  import { Effect, FileSystem, Option, Path, Schema } from "effect";
5
6
  import { ConceptId, Derive, Graph } from "@okfit/core";
6
7
  import { Tool } from "effect/unstable/ai";
@@ -32,8 +33,8 @@ const getConcept = Tool.make("get_concept", {
32
33
  /**
33
34
  * Deviation from the brief's literal snippet, matching the ruling binding
34
35
  * 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
36
+ * `ToolFailure.message` (`@effected/mcp`) at construction, since a declared
37
+ * typed failure under `failureMode: "error"` never reaches the wire with
37
38
  * `structuredContent` — only `error.message` does.
38
39
  *
39
40
  * @public
@@ -48,7 +49,7 @@ const handleGetConcept = (projectRoot, params) => Effect.gen(function* () {
48
49
  };
49
50
  return yield* Effect.fail(new InvalidArgument({
50
51
  argument: "id",
51
- message: composeRemediatedMessage("id must not be empty", remediation),
52
+ message: ToolFailure.message("id must not be empty", remediation),
52
53
  remediation
53
54
  }));
54
55
  }
@@ -61,7 +62,7 @@ const handleGetConcept = (projectRoot, params) => Effect.gen(function* () {
61
62
  };
62
63
  return yield* Effect.fail(new ConceptNotFound({
63
64
  id: params.id,
64
- message: composeRemediatedMessage(`no concept "${truncateEchoed(params.id)}" in this bundle`, remediation),
65
+ message: ToolFailure.message(`no concept "${ToolFailure.truncate(params.id)}" in this bundle`, remediation),
65
66
  remediation
66
67
  }));
67
68
  }
@@ -1,7 +1,8 @@
1
- import { McpToolError, UnknownVocabulary, composeRemediatedMessage, truncateEchoed } from "../errors.js";
1
+ import { McpToolError, UnknownVocabulary } from "../errors.js";
2
2
  import { loadToolContext } from "../internal/toolContext.js";
3
3
  import { toConceptSummary } from "../schema/ConceptSummary.js";
4
4
  import { ListConceptsParams, ListConceptsSuccess } from "../schema/tools.js";
5
+ import { ToolFailure } from "@effected/mcp";
5
6
  import { Effect, FileSystem, Path } from "effect";
6
7
  import { Derive } from "@okfit/core";
7
8
  import { Tool } from "effect/unstable/ai";
@@ -33,24 +34,24 @@ const listConcepts = Tool.make("list_concepts", {
33
34
  }).annotate(Tool.Title, "List OKF concepts").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
34
35
  /**
35
36
  * 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`.
37
+ * `ToolFailure.message` (`@effected/mcp`), and the raw message embeds the
38
+ * full `valid` list rather than leaving it only on the schema's `valid`
39
+ * field — a declared typed failure under `failureMode: "error"` never
40
+ * reaches the wire with `structuredContent` (B1's finding), so `valid`
41
+ * would otherwise be unreachable to a real caller reading only
42
+ * `content[0].text`.
42
43
  */
43
44
  const unknown = (kind, requested, valid) => {
44
45
  const remediation = {
45
46
  hint: "Use one of the listed type names, or call describe_vocabulary.",
46
47
  suggestedTool: "describe_vocabulary"
47
48
  };
48
- const rawMessage = `"${truncateEchoed(requested)}" is not a ${kind} declared by this project's okfit config. Valid ${kind}s: ${valid.join(", ")}.`;
49
+ const rawMessage = `"${ToolFailure.truncate(requested)}" is not a ${kind} declared by this project's okfit config. Valid ${kind}s: ${valid.join(", ")}.`;
49
50
  return new UnknownVocabulary({
50
51
  kind,
51
52
  requested,
52
53
  valid,
53
- message: composeRemediatedMessage(rawMessage, remediation),
54
+ message: ToolFailure.message(rawMessage, remediation),
54
55
  remediation
55
56
  });
56
57
  };
@@ -1,8 +1,9 @@
1
- import { BundleNotFound, InvalidArgument, McpToolError, composeRemediatedMessage } from "../errors.js";
1
+ import { BundleNotFound, InvalidArgument, McpToolError } from "../errors.js";
2
2
  import { resolveConfigOnly } from "../internal/toolContext.js";
3
3
  import { ValidateBundleParams } from "../schema/tools.js";
4
4
  import { resolveNow } from "../internal/resolveNow.js";
5
5
  import { MCP_VERSION } from "../version.js";
6
+ import { ToolFailure } from "@effected/mcp";
6
7
  import { Crypto, Effect, FileSystem, Option, Path } from "effect";
7
8
  import { OKF_SPEC_VERSION } from "@okfit/core";
8
9
  import { Tool } from "effect/unstable/ai";
@@ -80,14 +81,14 @@ const handleValidateBundle = (projectRoot, params, distribution) => Effect.gen(f
80
81
  const remediation = { hint: "Give each document path relative to the bundle root, in posix form, naming a .md file once (for example metrics/churn.md)." };
81
82
  return new InvalidArgument({
82
83
  argument: "documents",
83
- message: composeRemediatedMessage(cause.message, remediation),
84
+ message: ToolFailure.message(cause.message, remediation),
84
85
  remediation
85
86
  });
86
87
  }
87
88
  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\`.` };
88
89
  return new BundleNotFound({
89
90
  root: resolved.bundleRoot,
90
- message: composeRemediatedMessage(cause.message, remediation),
91
+ message: ToolFailure.message(cause.message, remediation),
91
92
  remediation
92
93
  });
93
94
  }));
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.59.2"
9
9
  }
10
10
  ]
11
11
  }
package/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @public
7
7
  */
8
- const MCP_VERSION = "0.5.3";
8
+ const MCP_VERSION = "0.6.0";
9
9
 
10
10
  //#endregion
11
11
  export { MCP_VERSION };