@effected/mcp 0.12.0 → 0.14.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 +5 -562
- package/bin/effected-mcp.js +12 -0
- package/index.d.ts +11 -979
- package/index.js +2 -9
- package/main.d.ts +28 -0
- package/main.js +46 -0
- package/package.json +17 -21
- package/server.js +48 -0
- package/tools/identity.js +28 -0
- package/tools/schema.js +158 -0
- package/tsdoc-metadata.json +1 -1
- package/version.js +11 -0
- package/McpGuard.js +0 -81
- package/McpHarness.js +0 -338
- package/McpProbe.js +0 -94
- package/McpProcess.js +0 -196
- package/McpStdio.js +0 -171
- package/McpTestFailure.js +0 -49
- package/McpToolAudit.js +0 -109
- package/McpToolkit.js +0 -257
- package/StdioCapture.js +0 -146
- package/ToolFailure.js +0 -75
- package/ToolInputSchema.js +0 -180
- package/ToolOutcome.js +0 -119
- package/ToolOutputSchema.js +0 -65
- package/ToolRefusal.js +0 -57
- package/ToolReplay.js +0 -129
- package/guard.d.ts +0 -189
- package/guard.js +0 -3
- package/internal/LaunchFailed.js +0 -34
- package/internal/StdinFrames.js +0 -201
- package/internal/guardLaunch.js +0 -13
- package/internal/wire.js +0 -65
- package/node.d.ts +0 -126
- package/node.js +0 -3
- package/testing.d.ts +0 -490
- package/testing.js +0 -7
package/index.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ToolFailure } from "./ToolFailure.js";
|
|
3
|
-
import { ToolInputSchema } from "./ToolInputSchema.js";
|
|
4
|
-
import { McpToolkit } from "./McpToolkit.js";
|
|
5
|
-
import { ToolOutputSchema } from "./ToolOutputSchema.js";
|
|
6
|
-
import { ToolOutcome } from "./ToolOutcome.js";
|
|
7
|
-
import { ToolRefusal } from "./ToolRefusal.js";
|
|
8
|
-
import { ToolReplay } from "./ToolReplay.js";
|
|
1
|
+
import { MCP_VERSION } from "./version.js";
|
|
9
2
|
|
|
10
|
-
export {
|
|
3
|
+
export { MCP_VERSION };
|
package/main.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Distribution } from "@effected/kit";
|
|
2
|
+
/**
|
|
3
|
+
* Options a carrier's bin shim passes to `main`. `distribution` names
|
|
4
|
+
* the package the `effected-mcp` bin was installed through; omitted for a
|
|
5
|
+
* direct install of `@effected/mcp`.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface MainOptions {
|
|
10
|
+
readonly distribution?: Distribution;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run the `effected-mcp` server over stdio. Owns the process.
|
|
14
|
+
*
|
|
15
|
+
* `McpGuard.run` installs the crash guards before `load` evaluates the server
|
|
16
|
+
* graph, so a throw during module evaluation is still reported on stderr; this
|
|
17
|
+
* module therefore carries no static runtime import (`Distribution` is
|
|
18
|
+
* type-only). Policy is `exitBeforeConnect` for both events: the server holds
|
|
19
|
+
* no state between calls, and a stray error while serving is likely a config's
|
|
20
|
+
* own (config modules run in this process), so the guard logs and keeps
|
|
21
|
+
* serving, while the same error before serving means a broken boot and exits
|
|
22
|
+
* `1`. Nothing here writes to stdout; it is the JSON-RPC wire. A config can, so
|
|
23
|
+
* a tool call runs under `StdioCapture`, its stdout diverted to stderr.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export declare const main: (options?: MainOptions) => Promise<void>;
|
|
28
|
+
//# sourceMappingURL=main.d.ts.map
|
package/main.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { McpGuard } from "@effected/mcp-kit/guard";
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
/**
|
|
5
|
+
* Run the `effected-mcp` server over stdio. Owns the process.
|
|
6
|
+
*
|
|
7
|
+
* `McpGuard.run` installs the crash guards before `load` evaluates the server
|
|
8
|
+
* graph, so a throw during module evaluation is still reported on stderr; this
|
|
9
|
+
* module therefore carries no static runtime import (`Distribution` is
|
|
10
|
+
* type-only). Policy is `exitBeforeConnect` for both events: the server holds
|
|
11
|
+
* no state between calls, and a stray error while serving is likely a config's
|
|
12
|
+
* own (config modules run in this process), so the guard logs and keeps
|
|
13
|
+
* serving, while the same error before serving means a broken boot and exits
|
|
14
|
+
* `1`. Nothing here writes to stdout; it is the JSON-RPC wire. A config can, so
|
|
15
|
+
* a tool call runs under `StdioCapture`, its stdout diverted to stderr.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
const main = (options = {}) => McpGuard.run({
|
|
20
|
+
label: "effected-mcp",
|
|
21
|
+
host: process,
|
|
22
|
+
policy: {
|
|
23
|
+
onUncaught: "exitBeforeConnect",
|
|
24
|
+
onRejection: "exitBeforeConnect"
|
|
25
|
+
},
|
|
26
|
+
load: async () => {
|
|
27
|
+
const NodeRuntime = await import("@effect/platform-node/NodeRuntime");
|
|
28
|
+
const NodeStdio = await import("@effect/platform-node/NodeStdio");
|
|
29
|
+
const NodeFileSystem = await import("@effect/platform-node/NodeFileSystem");
|
|
30
|
+
const NodePath = await import("@effect/platform-node/NodePath");
|
|
31
|
+
const { StdioCapture } = await import("@effected/mcp-kit/node");
|
|
32
|
+
const { Layer } = await import("effect");
|
|
33
|
+
const { ServerLayer } = await import("./server.js");
|
|
34
|
+
const capture = StdioCapture.install(process);
|
|
35
|
+
return {
|
|
36
|
+
layer: ServerLayer({
|
|
37
|
+
...options,
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
confineStdout: capture.capture({ stdout: capture.toStderr })
|
|
40
|
+
}).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer))),
|
|
41
|
+
runMain: NodeRuntime.runMain
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export { main };
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "The
|
|
5
|
+
"description": "The effected-mcp server: the tool's MCP tools on the kit's MCP boundary",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"effect",
|
|
8
8
|
"mcp",
|
|
9
9
|
"model-context-protocol",
|
|
10
|
-
"stdio",
|
|
11
|
-
"json-rpc",
|
|
12
10
|
"effected"
|
|
13
11
|
],
|
|
14
12
|
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/mcp#readme",
|
|
@@ -34,26 +32,24 @@
|
|
|
34
32
|
"import": "./index.js",
|
|
35
33
|
"default": "./index.js"
|
|
36
34
|
},
|
|
37
|
-
"./
|
|
38
|
-
"types": "./
|
|
39
|
-
"import": "./
|
|
40
|
-
"default": "./
|
|
41
|
-
},
|
|
42
|
-
"./node": {
|
|
43
|
-
"types": "./node.d.ts",
|
|
44
|
-
"import": "./node.js",
|
|
45
|
-
"default": "./node.js"
|
|
46
|
-
},
|
|
47
|
-
"./testing": {
|
|
48
|
-
"types": "./testing.d.ts",
|
|
49
|
-
"import": "./testing.js",
|
|
50
|
-
"default": "./testing.js"
|
|
35
|
+
"./main": {
|
|
36
|
+
"types": "./main.d.ts",
|
|
37
|
+
"import": "./main.js",
|
|
38
|
+
"default": "./main.js"
|
|
51
39
|
},
|
|
52
40
|
"./package.json": "./package.json"
|
|
53
41
|
},
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
42
|
+
"bin": {
|
|
43
|
+
"effected-mcp": "bin/effected-mcp.js"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@effect/platform-node": "~4.1.2",
|
|
47
|
+
"@effected/engine": "0.13.0",
|
|
48
|
+
"@effected/glob": "^0.19.0",
|
|
49
|
+
"@effected/kit": "^0.13.0",
|
|
50
|
+
"@effected/mcp-kit": "^0.14.0",
|
|
51
|
+
"@effected/walker": "^0.25.1",
|
|
52
|
+
"effect": "~4.1.2"
|
|
57
53
|
},
|
|
58
54
|
"engines": {
|
|
59
55
|
"node": ">=24.11.0"
|
package/server.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { handleIdentity, identity } from "./tools/identity.js";
|
|
2
|
+
import { makeSchemaHandlers, schemaBuild, schemaCheck, schemaValidate } from "./tools/schema.js";
|
|
3
|
+
import { MCP_VERSION } from "./version.js";
|
|
4
|
+
import { McpStdio, McpToolkit } from "@effected/mcp-kit";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { McpProtocol, Toolkit } from "effect/ai";
|
|
7
|
+
|
|
8
|
+
// src/server.ts
|
|
9
|
+
/**
|
|
10
|
+
* Agent-facing orientation, surfaced as `instructions` in both the
|
|
11
|
+
* `initialize` and `server/discover` results.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
const SERVER_INSTRUCTIONS = "effected-mcp serves the effected tool. Call identity to learn the tool name, this server's version, the distribution it was launched through and the resolved effect version. schema_check reports whether the generated schema documents match the config and writes nothing, schema_build writes them, and schema_validate checks a JSON payload against one. Drift, staleness and a failed validation arrive as results (a verdict, or valid: false), not as errors; an error means the config could not be found or loaded, a path was outside the workspace root, or the run could not finish. Each call reloads the config and the TypeScript and JSON it imports, but an imported .mjs, .cjs or built .js under type: module stays cached, so restart this server after rebuilding a sibling package the config uses.";
|
|
16
|
+
/**
|
|
17
|
+
* The whole server as one layer: the toolkit over the kit's `McpStdio.layer`,
|
|
18
|
+
* its schema handlers capturing `FileSystem` and `Path` when they are built.
|
|
19
|
+
* The adapters are listed newest first, as the kit's
|
|
20
|
+
* default lists them; a client that opens with `initialize` needs the
|
|
21
|
+
* stateful ones.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
const ServerLayer = (options = {}) => {
|
|
26
|
+
const EffectedToolkit = Toolkit.make(identity, schemaCheck, schemaBuild, schemaValidate);
|
|
27
|
+
const handlers = EffectedToolkit.toLayer(Effect.map(makeSchemaHandlers(options), (schema) => ({
|
|
28
|
+
identity: () => handleIdentity(options.distribution),
|
|
29
|
+
...schema
|
|
30
|
+
})));
|
|
31
|
+
return McpStdio.layer({
|
|
32
|
+
name: "effected",
|
|
33
|
+
version: MCP_VERSION,
|
|
34
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
35
|
+
cacheHints: { tools: {
|
|
36
|
+
ttl: "1 hour",
|
|
37
|
+
cacheScope: "private"
|
|
38
|
+
} },
|
|
39
|
+
protocols: [
|
|
40
|
+
McpProtocol.v2026_07_28,
|
|
41
|
+
McpProtocol.v2025_11_25,
|
|
42
|
+
McpProtocol.v2025_06_18
|
|
43
|
+
],
|
|
44
|
+
registrations: McpToolkit.layer(EffectedToolkit).pipe(Layer.provide(handlers))
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { SERVER_INSTRUCTIONS, ServerLayer };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MCP_VERSION } from "../version.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Tool } from "effect/ai";
|
|
4
|
+
import { Identity, IdentityReport } from "@effected/engine";
|
|
5
|
+
|
|
6
|
+
// src/tools/identity.ts
|
|
7
|
+
/**
|
|
8
|
+
* The `identity` tool: what the `effected` tool reports about itself, the
|
|
9
|
+
* same report the `effected` command prints.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
const identity = Tool.make("identity", {
|
|
14
|
+
description: "Reports the effected tool's identity: the tool name, this server's version, the carrier distribution it was launched through (null for a direct install), and the resolved effect version.",
|
|
15
|
+
parameters: Tool.EmptyParams,
|
|
16
|
+
success: IdentityReport
|
|
17
|
+
}).annotate(Tool.Title, "Effected identity").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
18
|
+
/**
|
|
19
|
+
* The `identity` handler, closed over the carrier distribution.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
const handleIdentity = (distribution) => Effect.suspend(() => Identity.report({
|
|
24
|
+
version: MCP_VERSION,
|
|
25
|
+
distribution: distribution ?? null
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
export { handleIdentity, identity };
|
package/tools/schema.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { ToolFailure, ToolRefusal } from "@effected/mcp-kit";
|
|
2
|
+
import { Effect, FileSystem, Option, Path, Ref, Schema } from "effect";
|
|
3
|
+
import { Tool } from "effect/ai";
|
|
4
|
+
import { ConfigLoader, SchemaRun, SchemaRunResult, SchemaValidate, ValidateReport } from "@effected/engine/schema";
|
|
5
|
+
import { Exclusive, PathContainment } from "@effected/kit";
|
|
6
|
+
import { Walker } from "@effected/walker";
|
|
7
|
+
|
|
8
|
+
// src/tools/schema.ts
|
|
9
|
+
const CONFIG_DESCRIPTION = "The schemastore config, resolved against the workspace root and refused outside it. Omitted, discovery walks upward from the server's working directory, stopping at the workspace root.";
|
|
10
|
+
const RunParameters = Schema.Struct({
|
|
11
|
+
config: Schema.optionalKey(Schema.String.annotate({ description: CONFIG_DESCRIPTION })),
|
|
12
|
+
drift: Schema.optionalKey(Schema.Literals([
|
|
13
|
+
"strict",
|
|
14
|
+
"semantic",
|
|
15
|
+
"allow"
|
|
16
|
+
]).annotate({ description: "One drift tolerance over every schema's own." })),
|
|
17
|
+
onDrift: Schema.optionalKey(Schema.Literals(["error", "warn"]).annotate({ description: "Overrides the config's onDrift." })),
|
|
18
|
+
force: Schema.optionalKey(Schema.Boolean.annotate({ description: "Shorthand for drift \"allow\", and refused beside any other drift." }))
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* The `schema_check` tool: what `schema_build` would do, writing nothing.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
const schemaCheck = Tool.make("schema_check", {
|
|
26
|
+
description: "Checks the generated schema documents against the config, writing nothing. Drift and staleness arrive as the result's verdict, with each changed document's full diff; they are not errors. Reloads the config on every call.",
|
|
27
|
+
parameters: RunParameters,
|
|
28
|
+
success: SchemaRunResult,
|
|
29
|
+
failure: ToolRefusal
|
|
30
|
+
}).annotate(Tool.Title, "Check schema documents").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
31
|
+
/**
|
|
32
|
+
* The `schema_build` tool: writes the generated documents and catalogs.
|
|
33
|
+
*
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
const schemaBuild = Tool.make("schema_build", {
|
|
37
|
+
description: "Generates the schema documents and catalogs the config declares and writes the ones that changed, wherever the config puts them. A refused drift or a failed gate writes nothing and arrives as the result's verdict. Reloads the config on every call.",
|
|
38
|
+
parameters: RunParameters,
|
|
39
|
+
success: SchemaRunResult,
|
|
40
|
+
failure: ToolRefusal
|
|
41
|
+
}).annotate(Tool.Title, "Build schema documents").annotate(Tool.Readonly, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
42
|
+
/**
|
|
43
|
+
* The `schema_validate` tool: does one payload conform to a published document.
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
const schemaValidate = Tool.make("schema_validate", {
|
|
48
|
+
description: "Validates a JSON payload file against a local schema document, with each finding's line and column. A payload that does not conform is valid: false, not an error.",
|
|
49
|
+
parameters: Schema.Struct({
|
|
50
|
+
payload: Schema.String.annotate({ description: "The payload file, resolved against the workspace root and refused outside it." }),
|
|
51
|
+
config: Schema.optionalKey(Schema.String.annotate({ description: CONFIG_DESCRIPTION })),
|
|
52
|
+
schema: Schema.optionalKey(Schema.String.annotate({ description: "The document: a file path under the workspace root, or an $id or url the config derives. Omitted, the payload's own $schema." }))
|
|
53
|
+
}),
|
|
54
|
+
success: ValidateReport,
|
|
55
|
+
failure: ToolRefusal
|
|
56
|
+
}).annotate(Tool.Title, "Validate a payload").annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
57
|
+
const WALK_TIMEOUT = "2 minutes";
|
|
58
|
+
const findRoot = (cwd) => Effect.gen(function* () {
|
|
59
|
+
const fs = yield* FileSystem.FileSystem;
|
|
60
|
+
const path = yield* Path.Path;
|
|
61
|
+
const chain = yield* Walker.ascend(cwd);
|
|
62
|
+
const holding = (marker) => Walker.findRoot(chain, (dir) => fs.exists(path.join(dir, marker)));
|
|
63
|
+
const workspace = yield* holding("pnpm-workspace.yaml");
|
|
64
|
+
return Option.isSome(workspace) ? workspace.value : Option.getOrElse(yield* holding(".git"), () => cwd);
|
|
65
|
+
});
|
|
66
|
+
const refusalOf = (error, root, mismatch) => {
|
|
67
|
+
const refuse = (reason, hint, suggestedTool) => ToolRefusal.refuse([ToolFailure.truncate(reason, ToolFailure.ENGINE_ECHO_LIMIT), ...mismatch !== void 0 ? [`Version mismatch: ${mismatch.message}`] : []].join(" "), {
|
|
68
|
+
hint,
|
|
69
|
+
...suggestedTool !== void 0 ? { suggestedTool } : {}
|
|
70
|
+
});
|
|
71
|
+
switch (error._tag) {
|
|
72
|
+
case "ConfigNotFoundError": return refuse(error.message, `Add a schemastore.config.ts, or pass config with its path under the workspace root ${root}.`);
|
|
73
|
+
case "ConfigLoadError": return refuse(`Failed to load schemastore config ${error.path}: ${error.reason.split("\n", 1)[0]}`, "Fix the config and call the tool again; it reloads on every call.");
|
|
74
|
+
case "ConflictingFlagsError": return refuse(`force means drift "allow", so it conflicts with drift "${error.policy}".`, "Pass one or the other.");
|
|
75
|
+
case "MissingSchemaRefError": return refuse(`Payload ${error.path} has no "$schema" property.`, "Name the document with the schema parameter: a file path, or an $id or url the config derives.");
|
|
76
|
+
case "ExclusiveTimeout": return refuse(error.message, "Documents may be part-written; call the tool to see what differs.", "schema_check");
|
|
77
|
+
default: return refuse(error.message, "Fix what the message names and call the tool again.");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const payloadRef = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ $schema: Schema.String })));
|
|
81
|
+
/**
|
|
82
|
+
* The three handlers, closed over the filesystem and path the server was built with, the workspace root found once
|
|
83
|
+
* from `options.cwd`, and one lane.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* `schema_build` and `schema_check` load the config OUTSIDE the lane and hand the engine what was loaded. A timed-out
|
|
87
|
+
* lane interrupts its body, and the engine's `Effect.tryPromise` around the import abandons the promise on interrupt,
|
|
88
|
+
* so a load inside would leave a config still evaluating while the next call holds the permit. A hanging config
|
|
89
|
+
* therefore never holds up another call. `schema_validate` stays out: it only reads one document, and each document is
|
|
90
|
+
* replaced atomically, so it sees the old text or the new.
|
|
91
|
+
*
|
|
92
|
+
* @public
|
|
93
|
+
*/
|
|
94
|
+
const makeSchemaHandlers = (options = {}) => Effect.gen(function* () {
|
|
95
|
+
const fs = yield* FileSystem.FileSystem;
|
|
96
|
+
const path = yield* Path.Path;
|
|
97
|
+
const cwd = path.resolve(options.cwd ?? ".");
|
|
98
|
+
const root = yield* findRoot(cwd);
|
|
99
|
+
const lane = yield* Exclusive.make;
|
|
100
|
+
const importer = options.importModule !== void 0 ? { importModule: options.importModule } : {};
|
|
101
|
+
const inside = (label, candidate) => Effect.flatMap(PathContainment.isWithin(root, candidate), (within) => within ? Effect.void : Effect.fail(ToolRefusal.refuse(`The ${label} "${ToolFailure.truncate(candidate)}" is outside the workspace root ${root}.`, { hint: `Pass a path under ${root}.` })));
|
|
102
|
+
const configOf = (config) => config !== void 0 ? Effect.as(inside("config", config), { config }) : Effect.match(ConfigLoader.discover(cwd, root), {
|
|
103
|
+
onFailure: () => ({}),
|
|
104
|
+
onSuccess: (found) => ({ config: found })
|
|
105
|
+
});
|
|
106
|
+
const call = (run) => Effect.gen(function* () {
|
|
107
|
+
const warned = yield* Ref.make(void 0);
|
|
108
|
+
return yield* run((event) => event._tag === "Started" && event.versionMismatch !== void 0 ? Ref.set(warned, event.versionMismatch) : Effect.void).pipe(Effect.catch((error) => error._tag === "ToolRefusal" ? Effect.fail(error) : Effect.gen(function* () {
|
|
109
|
+
yield* Effect.logDebug("schema tool refused", { error: error.message });
|
|
110
|
+
return yield* Effect.fail(refusalOf(error, root, yield* Ref.get(warned)));
|
|
111
|
+
})));
|
|
112
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), options.confineStdout ?? ((self) => self));
|
|
113
|
+
const walk = (mode) => (params) => call((onProgress) => Effect.gen(function* () {
|
|
114
|
+
const scoped = yield* configOf(params.config);
|
|
115
|
+
const loaded = yield* ConfigLoader.load({
|
|
116
|
+
cwd: root,
|
|
117
|
+
ceiling: root,
|
|
118
|
+
...scoped.config !== void 0 ? { explicit: scoped.config } : {},
|
|
119
|
+
...importer
|
|
120
|
+
});
|
|
121
|
+
return yield* lane.run(SchemaRun.run({
|
|
122
|
+
mode,
|
|
123
|
+
config: loaded.path,
|
|
124
|
+
...params.drift !== void 0 ? { drift: params.drift } : {},
|
|
125
|
+
...params.onDrift !== void 0 ? { onDrift: params.onDrift } : {},
|
|
126
|
+
force: params.force ?? false,
|
|
127
|
+
versionMismatch: "warn"
|
|
128
|
+
}, {
|
|
129
|
+
cwd: root,
|
|
130
|
+
discoveryCeiling: root,
|
|
131
|
+
importModule: () => Promise.resolve(loaded.config),
|
|
132
|
+
onProgress
|
|
133
|
+
}), { timeout: WALK_TIMEOUT });
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
schema_check: walk("check"),
|
|
137
|
+
schema_build: walk("build"),
|
|
138
|
+
schema_validate: (params) => call((onProgress) => Effect.gen(function* () {
|
|
139
|
+
yield* inside("payload", params.payload);
|
|
140
|
+
const ref = params.schema ?? Option.getOrUndefined(Option.map(yield* fs.readFileString(path.resolve(root, params.payload)).pipe(Effect.map((text) => payloadRef(text)), Effect.orElseSucceed(() => Option.none())), (payload) => payload.$schema));
|
|
141
|
+
if (ref !== void 0) yield* inside("schema", ref);
|
|
142
|
+
const scoped = yield* configOf(params.config);
|
|
143
|
+
return yield* SchemaValidate.run({
|
|
144
|
+
payload: params.payload,
|
|
145
|
+
...ref !== void 0 ? { schema: ref } : {},
|
|
146
|
+
...scoped,
|
|
147
|
+
versionMismatch: "warn"
|
|
148
|
+
}, {
|
|
149
|
+
cwd: root,
|
|
150
|
+
discoveryCeiling: root,
|
|
151
|
+
...importer,
|
|
152
|
+
onProgress
|
|
153
|
+
});
|
|
154
|
+
}))
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export { makeSchemaHandlers, schemaBuild, schemaCheck, schemaValidate };
|
package/tsdoc-metadata.json
CHANGED
package/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `effected-mcp` server's version. The bundler replaces the exact expression
|
|
4
|
+
* `process.env.__PACKAGE_VERSION__` with the package version at build time, so
|
|
5
|
+
* it must stay spelled this way.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
const MCP_VERSION = "0.14.0";
|
|
10
|
+
|
|
11
|
+
export { MCP_VERSION };
|
package/McpGuard.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { ProcessGuard } from "@effected/engine/guard";
|
|
2
|
-
|
|
3
|
-
// src/McpGuard.ts
|
|
4
|
-
/**
|
|
5
|
-
* Crash guards for an MCP server process, installed before the server's
|
|
6
|
-
* module graph is loaded. Imported from `@effected/mcp/guard`.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* {@link McpGuard.run} is `ProcessGuard.run` from `@effected/engine/guard`
|
|
10
|
-
* with an MCP launch in its `load`: it registers `uncaughtException` and
|
|
11
|
-
* `unhandledRejection` listeners on `host`, then awaits `load()`, then
|
|
12
|
-
* launches the server with `McpStdio.launch` and `McpStdio.teardown` under
|
|
13
|
-
* the `runMain` it returned. A server on another transport (an LSP over
|
|
14
|
-
* `vscode-languageserver`, say) uses `ProcessGuard.run` directly and calls
|
|
15
|
-
* its `markConnected` itself. This entrypoint statically imports only
|
|
16
|
-
* `@effected/engine/guard`, which imports nothing: it loads `effect` and the
|
|
17
|
-
* stdio wiring only after the guards are listening, so a throw while any of
|
|
18
|
-
* it evaluates is still reported on stderr.
|
|
19
|
-
*
|
|
20
|
-
* - "Connected" means the whole server layer has built, so stdin is being
|
|
21
|
-
* read. That is before any client sends `initialize`.
|
|
22
|
-
* - `injectCrash` drives either half of the policy from a test, before
|
|
23
|
-
* `load()` or once connected; unset, it does nothing.
|
|
24
|
-
* - A `load()` that rejects is reported as `startup failed` and exits 1
|
|
25
|
-
* whatever the policy. Left to a log-only rejection listener, it would
|
|
26
|
-
* let the event loop drain and exit 0 with no server.
|
|
27
|
-
* - Installing an `unhandledRejection` listener switches off Node's default
|
|
28
|
-
* of throwing on one, so `"log"` really does keep the process running.
|
|
29
|
-
* - An exit from a guard skips Effect's finalizers and the teardown.
|
|
30
|
-
* - Keeping on serving after a crash works because the platform `runMain`
|
|
31
|
-
* holds the process open while the server fiber lives.
|
|
32
|
-
* - A layer that fails to build is still reported by `McpStdio.launch`.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```ts
|
|
36
|
-
* import { McpGuard } from "@effected/mcp/guard";
|
|
37
|
-
*
|
|
38
|
-
* await McpGuard.run({
|
|
39
|
-
* label: "my-server",
|
|
40
|
-
* host: process,
|
|
41
|
-
* policy: { onUncaught: "exitBeforeConnect", onRejection: "log" },
|
|
42
|
-
* load: async () => {
|
|
43
|
-
* const { NodeRuntime } = await import("@effect/platform-node");
|
|
44
|
-
* const { Main } = await import("./server.js");
|
|
45
|
-
* return { layer: Main, runMain: NodeRuntime.runMain };
|
|
46
|
-
* },
|
|
47
|
-
* });
|
|
48
|
-
* ```
|
|
49
|
-
*
|
|
50
|
-
* @public
|
|
51
|
-
*/
|
|
52
|
-
var McpGuard = class {
|
|
53
|
-
constructor() {}
|
|
54
|
-
/**
|
|
55
|
-
* Parse a test-only crash-injection setting into {@link McpGuardRunOptions.injectCrash}: `<at>:<kind>`, where `at`
|
|
56
|
-
* is `"load"` or `"connected"` and `kind` is `"uncaughtException"` or `"unhandledRejection"`. Anything else, or no
|
|
57
|
-
* value, is `undefined`. The same grammar as `ProcessGuard.parseInjectCrash` from `@effected/engine/guard`, which
|
|
58
|
-
* this is, so an MCP launcher need not import the engine guard for it.
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```ts
|
|
62
|
-
* McpGuard.parseInjectCrash("load:uncaughtException"); // => { at: "load", kind: "uncaughtException" }
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
static parseInjectCrash = ProcessGuard.parseInjectCrash;
|
|
66
|
-
/** Install the guards, load the server, and launch it. Resolves once the server is launched. */
|
|
67
|
-
static run = (options) => ProcessGuard.run({
|
|
68
|
-
label: options.label,
|
|
69
|
-
host: options.host,
|
|
70
|
-
policy: options.policy,
|
|
71
|
-
injectCrash: options.injectCrash,
|
|
72
|
-
load: async (guard) => {
|
|
73
|
-
const server = await options.load();
|
|
74
|
-
if (server.format !== void 0) guard.useFormat(server.format);
|
|
75
|
-
const { launchGuarded } = await import("./internal/guardLaunch.js");
|
|
76
|
-
launchGuarded(server, guard.markConnected);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export { McpGuard };
|