@noetaris/harness-mcp 0.1.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 ADDED
@@ -0,0 +1,258 @@
1
+ # @noetaris/harness-mcp
2
+
3
+ MCP (Model Context Protocol) client integration for [@noetaris/harness](https://github.com/noetaris-lab/harness). Connect your agents to MCP servers over HTTP or stdio and expose their tools directly to the harness tool-calling pipeline.
4
+
5
+ ## Overview
6
+
7
+ `@noetaris/harness-mcp` provides two main building blocks:
8
+
9
+ - **`MCPClient`** — connects to a single MCP server (HTTP or stdio), discovers its tools, and exposes them as `Tool[]` compatible with the harness tool schema
10
+ - **`MCPManager`** — manages a pool of `MCPClient` instances, merges their tool lists, and supports live config reload via file watching
11
+
12
+ Config files (`.json`, `.ts`, `.js`, `.mjs`) can be loaded with `MCPManager.fromConfig()` or watched for changes with `MCPManager.watch()`.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @noetaris/harness-mcp
18
+ ```
19
+
20
+ Peer dependency:
21
+
22
+ ```bash
23
+ pnpm add @noetaris/harness-types
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { MCPClient, MCPManager } from '@noetaris/harness-mcp'
30
+
31
+ // single HTTP server
32
+ const client = await MCPClient.fromHttp('http://localhost:3100/mcp')
33
+ console.log(client.tools()) // Tool[]
34
+
35
+ // multiple servers via manager
36
+ const manager = new MCPManager([client])
37
+ await manager.addServer({ command: 'npx', args: ['my-mcp-server'] })
38
+ console.log(manager.tools()) // merged Tool[] — last-write-wins on duplicate names
39
+ ```
40
+
41
+ ## API Reference
42
+
43
+ ### `MCPClient`
44
+
45
+ Wraps a single MCP server connection. Use the static factory methods to create instances; the constructor is private.
46
+
47
+ #### `MCPClient.fromHttp(url, options?): Promise<MCPClient>`
48
+
49
+ Connects to an MCP server over Streamable HTTP and discovers its tools.
50
+
51
+ ```typescript
52
+ const client = await MCPClient.fromHttp('http://localhost:3100/mcp', {
53
+ prefix: 'search', // optional: prefix all tool names with "search/"
54
+ rediscover: 'per-session', // optional: re-run tool discovery on each harness session start
55
+ })
56
+ ```
57
+
58
+ | Option | Type | Description |
59
+ |--------|------|-------------|
60
+ | `prefix` | `string` | Prepended to all tool names as `{prefix}/{name}`. Useful to namespace tools from different servers. |
61
+ | `rediscover` | `"per-session"` | Re-discovers tools at the start of each harness run (via `bindObserver`). Omit for static tool lists. |
62
+
63
+ #### `MCPClient.fromStdio(params): Promise<MCPClient>`
64
+
65
+ Launches a subprocess and connects to it over stdio.
66
+
67
+ ```typescript
68
+ const client = await MCPClient.fromStdio({
69
+ command: 'npx',
70
+ args: ['@acme/mcp-server', '--port', '0'],
71
+ env: { API_KEY: process.env.API_KEY! },
72
+ prefix: 'acme',
73
+ })
74
+ ```
75
+
76
+ | Parameter | Type | Description |
77
+ |-----------|------|-------------|
78
+ | `command` | `string` | The executable to run. |
79
+ | `args` | `string[]` | Arguments passed to the command. |
80
+ | `env` | `Record<string, string>` | Environment variables for the subprocess. |
81
+ | `prefix` | `string` | Tool name prefix (same as HTTP). |
82
+ | `rediscover` | `"per-session"` | Per-session rediscovery (same as HTTP). |
83
+
84
+ #### `client.tools(): Tool[]`
85
+
86
+ Returns the cached tool list from the last `discover()` call.
87
+
88
+ #### `client.discover(): Promise<void>`
89
+
90
+ Re-queries the server for its tool list and updates the cache. Called automatically on construction; call manually to refresh.
91
+
92
+ #### `client.disconnect(): Promise<void>`
93
+
94
+ Closes the underlying transport. Always call this when the client is no longer needed to avoid resource leaks.
95
+
96
+ ---
97
+
98
+ ### `MCPManager`
99
+
100
+ Manages a pool of `MCPClient` instances. Tool name conflicts are resolved last-write-wins (later clients in the pool overwrite earlier ones for the same tool name).
101
+
102
+ #### `new MCPManager(clients, options?)`
103
+
104
+ ```typescript
105
+ const manager = new MCPManager([clientA, clientB], {
106
+ rediscover: 'per-session', // applies to all clients that don't set their own rediscover option
107
+ })
108
+ ```
109
+
110
+ #### `manager.tools(): Tool[]`
111
+
112
+ Returns the merged tool list across all connected servers.
113
+
114
+ #### `manager.addServer(params): Promise<void>`
115
+
116
+ Connects to a new server and adds it to the pool. Accepts the same parameters as `MCPClient.fromHttp` (pass `{ url: '...' }`) or `MCPClient.fromStdio` (pass `{ command: '...' }`).
117
+
118
+ ```typescript
119
+ await manager.addServer({ url: 'http://localhost:3200/mcp', prefix: 'tools' })
120
+ await manager.addServer({ command: 'my-mcp-server', prefix: 'local' })
121
+ ```
122
+
123
+ #### `manager.removeServer(key): Promise<void>`
124
+
125
+ Disconnects and removes a server by its URL (HTTP) or command (stdio). Throws `MCPServerNotFoundError` if no server matches the key.
126
+
127
+ ```typescript
128
+ await manager.removeServer('http://localhost:3200/mcp')
129
+ await manager.removeServer('my-mcp-server')
130
+ ```
131
+
132
+ #### `manager.loadConfig(path): Promise<void>`
133
+
134
+ Loads server definitions from a config file and adds each as a new server. See [Config File Format](#config-file-format) below.
135
+
136
+ #### `manager.watch(path, options?): Promise<() => Promise<void>>`
137
+
138
+ Watches a config file for changes and hot-reloads servers. Changes are debounced by 100ms. Returns a `dispose` function to stop watching.
139
+
140
+ ```typescript
141
+ const stopWatching = await manager.watch('./mcp.config.json', {
142
+ onError: (err) => console.error('MCP reload error:', err),
143
+ })
144
+
145
+ // later
146
+ await stopWatching()
147
+ ```
148
+
149
+ If a reload fails to add a new server, previously added servers from that reload batch are rolled back before `onError` is called.
150
+
151
+ #### `manager.bindObserver(observer): void`
152
+
153
+ Binds the manager to a harness observer. Clients configured with `rediscover: 'per-session'` will call `discover()` at the start of each harness run. Pass the observer you register with `harness.addObserver()`.
154
+
155
+ ```typescript
156
+ const manager = new MCPManager([client], { rediscover: 'per-session' })
157
+ harness.addObserver(manager.bindObserver.bind(manager))
158
+ ```
159
+
160
+ #### `MCPManager.fromConfig(path): Promise<MCPManager>`
161
+
162
+ Static factory. Creates a new manager and loads servers from a config file in one step.
163
+
164
+ ```typescript
165
+ const manager = await MCPManager.fromConfig('./mcp.config.json')
166
+ ```
167
+
168
+ ---
169
+
170
+ ### Config File Format
171
+
172
+ Config files can be `.json`, `.ts`, `.js`, or `.mjs`. The schema is:
173
+
174
+ ```typescript
175
+ interface MCPConfigSchema {
176
+ servers: MCPServerEntry[]
177
+ }
178
+ ```
179
+
180
+ **JSON example:**
181
+
182
+ ```json
183
+ {
184
+ "servers": [
185
+ { "url": "http://localhost:3100/mcp", "prefix": "search" },
186
+ {
187
+ "transport": "stdio",
188
+ "command": "npx",
189
+ "args": ["@acme/mcp-server"],
190
+ "env": { "API_KEY": "secret" },
191
+ "prefix": "acme",
192
+ "rediscover": "per-session"
193
+ }
194
+ ]
195
+ }
196
+ ```
197
+
198
+ **TypeScript example (`mcp.config.ts`):**
199
+
200
+ ```typescript
201
+ export default {
202
+ servers: [
203
+ { url: 'http://localhost:3100/mcp' },
204
+ { transport: 'stdio' as const, command: 'my-server' },
205
+ ],
206
+ }
207
+ ```
208
+
209
+ HTTP entries default to `transport: 'http'` and require a `url` field. Stdio entries require `transport: 'stdio'` and a `command` field. Both support optional `prefix` and `rediscover` fields.
210
+
211
+ ---
212
+
213
+ ### Error Classes
214
+
215
+ #### `MCPServerNotFoundError`
216
+
217
+ Thrown by `removeServer()` when no client matches the given key.
218
+
219
+ ```typescript
220
+ import { MCPServerNotFoundError } from '@noetaris/harness-mcp'
221
+
222
+ try {
223
+ await manager.removeServer('http://gone.invalid')
224
+ } catch (err) {
225
+ if (err instanceof MCPServerNotFoundError) {
226
+ console.error(err.key) // 'http://gone.invalid'
227
+ }
228
+ }
229
+ ```
230
+
231
+ #### `MCPConfigParseError`
232
+
233
+ Thrown by `loadConfig()` and `watch()` when the config file is structurally invalid.
234
+
235
+ ```typescript
236
+ import { MCPConfigParseError } from '@noetaris/harness-mcp'
237
+
238
+ try {
239
+ await manager.loadConfig('./bad-config.json')
240
+ } catch (err) {
241
+ if (err instanceof MCPConfigParseError) {
242
+ console.error(err.path) // file path
243
+ console.error(err.detail) // what was wrong
244
+ }
245
+ }
246
+ ```
247
+
248
+ #### `MCPConfigExtensionError`
249
+
250
+ Thrown when the config file has an unsupported extension.
251
+
252
+ ```typescript
253
+ import { MCPConfigExtensionError } from '@noetaris/harness-mcp'
254
+ ```
255
+
256
+ ## License
257
+
258
+ MIT
@@ -0,0 +1,109 @@
1
+ import { Tool } from '@noetaris/harness-types';
2
+
3
+ interface MCPClientOptions {
4
+ prefix?: string;
5
+ rediscover?: "per-session";
6
+ }
7
+ interface MCPStdioParams extends MCPClientOptions {
8
+ command: string;
9
+ args?: string[];
10
+ env?: Record<string, string>;
11
+ }
12
+ type TransportKind = "http" | "stdio";
13
+ type MCPCallToolResult = {
14
+ content: Array<{
15
+ type: string;
16
+ text?: string;
17
+ [key: string]: unknown;
18
+ }>;
19
+ isError?: boolean;
20
+ };
21
+ declare class MCPNotConnectedError extends Error {
22
+ constructor();
23
+ }
24
+ declare class MCPClient {
25
+ readonly url: string | undefined;
26
+ readonly command: string | undefined;
27
+ readonly options: MCPClientOptions;
28
+ readonly transportKind: TransportKind;
29
+ private sdk;
30
+ private cachedTools;
31
+ private connected;
32
+ private constructor();
33
+ static fromHttp(url: string, options?: MCPClientOptions): Promise<MCPClient>;
34
+ static fromStdio(params: MCPStdioParams): Promise<MCPClient>;
35
+ discover(): Promise<void>;
36
+ tools(): Tool[];
37
+ callTool(params: {
38
+ name: string;
39
+ arguments?: Record<string, unknown>;
40
+ }): Promise<MCPCallToolResult>;
41
+ disconnect(): Promise<void>;
42
+ }
43
+
44
+ interface MCPManagerOptions {
45
+ rediscover?: 'per-session';
46
+ }
47
+ interface MCPWatchOptions {
48
+ onError?: (err: Error) => void;
49
+ }
50
+ interface MCPHttpParams extends MCPClientOptions {
51
+ url: string;
52
+ }
53
+ type LocalObserver = {
54
+ onRunStart?: (...args: unknown[]) => void;
55
+ onRunEnd?: (...args: unknown[]) => void;
56
+ onStepStart?: (...args: unknown[]) => void;
57
+ onStepEnd?: (...args: unknown[]) => void;
58
+ onStepError?: (...args: unknown[]) => void;
59
+ onInterrupt?: (...args: unknown[]) => void;
60
+ onEvent?: (...args: unknown[]) => void;
61
+ };
62
+ declare class MCPServerNotFoundError extends Error {
63
+ readonly key: string;
64
+ constructor(key: string);
65
+ }
66
+ declare class MCPManager {
67
+ private clients;
68
+ private readonly options;
69
+ constructor(clients: MCPClient[], options?: MCPManagerOptions);
70
+ tools(): Tool[];
71
+ addServer(params: MCPHttpParams | MCPStdioParams): Promise<void>;
72
+ removeServer(key: string): Promise<void>;
73
+ bindObserver(_observer: LocalObserver): void;
74
+ loadConfig(path: string): Promise<void>;
75
+ watch(path: string, options?: MCPWatchOptions): Promise<() => Promise<void>>;
76
+ static fromConfig(path: string): Promise<MCPManager>;
77
+ private shouldRediscover;
78
+ }
79
+
80
+ interface MCPHttpEntry {
81
+ transport?: 'http';
82
+ url: string;
83
+ prefix?: string;
84
+ rediscover?: 'per-session';
85
+ }
86
+ interface MCPStdioEntry {
87
+ transport: 'stdio';
88
+ command: string;
89
+ args?: string[];
90
+ env?: Record<string, string>;
91
+ prefix?: string;
92
+ rediscover?: 'per-session';
93
+ }
94
+ type MCPServerEntry = MCPHttpEntry | MCPStdioEntry;
95
+ interface MCPConfigSchema {
96
+ servers: MCPServerEntry[];
97
+ }
98
+ declare class MCPConfigParseError extends Error {
99
+ readonly path: string;
100
+ readonly detail: string;
101
+ constructor(path: string, detail: string);
102
+ }
103
+ declare class MCPConfigExtensionError extends Error {
104
+ readonly path: string;
105
+ readonly extension: string;
106
+ constructor(path: string, extension: string);
107
+ }
108
+
109
+ export { type MCPCallToolResult, MCPClient, type MCPClientOptions, MCPConfigExtensionError, MCPConfigParseError, type MCPConfigSchema, type MCPHttpEntry, type MCPHttpParams, MCPManager, type MCPManagerOptions, MCPNotConnectedError, type MCPServerEntry, MCPServerNotFoundError, type MCPStdioEntry, type MCPStdioParams, type MCPWatchOptions };
package/dist/index.js ADDED
@@ -0,0 +1,353 @@
1
+ // src/mcp-client.ts
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ var MCPNotConnectedError = class extends Error {
6
+ constructor() {
7
+ super("not connected: client has been disconnected");
8
+ this.name = "MCPNotConnectedError";
9
+ }
10
+ };
11
+ var MCPClient = class _MCPClient {
12
+ url;
13
+ command;
14
+ options;
15
+ transportKind;
16
+ sdk;
17
+ cachedTools = [];
18
+ connected = true;
19
+ constructor(transportKind, options, url, command) {
20
+ this.transportKind = transportKind;
21
+ this.options = options;
22
+ this.url = url;
23
+ this.command = command;
24
+ this.sdk = new Client({ name: "@noetaris/harness-mcp", version: "0.1.0" });
25
+ }
26
+ static async fromHttp(url, options = {}) {
27
+ const client = new _MCPClient("http", options, url, void 0);
28
+ const transport = new StreamableHTTPClientTransport(new URL(url));
29
+ await client.sdk.connect(transport);
30
+ await client.discover();
31
+ return client;
32
+ }
33
+ static async fromStdio(params) {
34
+ const { command, args, env, ...options } = params;
35
+ const client = new _MCPClient("stdio", options, void 0, command);
36
+ const stdioParams = { command, ...args !== void 0 && { args }, ...env !== void 0 && { env } };
37
+ const transport = new StdioClientTransport(stdioParams);
38
+ await client.sdk.connect(transport);
39
+ await client.discover();
40
+ return client;
41
+ }
42
+ async discover() {
43
+ const result = await this.sdk.listTools();
44
+ const prefix = this.options.prefix;
45
+ this.cachedTools = result.tools.map((t) => ({
46
+ name: prefix !== void 0 ? `${prefix}/${t.name}` : t.name,
47
+ description: t.description ?? "",
48
+ inputSchema: t.inputSchema
49
+ // as: MCP SDK types inputSchema as a specific JSON Schema type; harness Tool uses the wider Record<string, unknown>
50
+ }));
51
+ }
52
+ tools() {
53
+ if (!this.connected) {
54
+ throw new MCPNotConnectedError();
55
+ }
56
+ return this.cachedTools;
57
+ }
58
+ async callTool(params) {
59
+ if (!this.connected) {
60
+ throw new MCPNotConnectedError();
61
+ }
62
+ const result = await this.sdk.callTool(params);
63
+ return result;
64
+ }
65
+ async disconnect() {
66
+ this.connected = false;
67
+ await this.sdk.close();
68
+ }
69
+ };
70
+
71
+ // src/mcp-manager.ts
72
+ import { watch as fsWatch } from "fs";
73
+ import { resolve as resolve2 } from "path";
74
+
75
+ // src/mcp-config-loader.ts
76
+ import { readFileSync } from "fs";
77
+ import { resolve, extname } from "path";
78
+ import { pathToFileURL } from "url";
79
+ var MCPConfigParseError = class extends Error {
80
+ path;
81
+ detail;
82
+ constructor(path, detail) {
83
+ super(`invalid MCP config at ${path}: ${detail}`);
84
+ this.name = "MCPConfigParseError";
85
+ this.path = path;
86
+ this.detail = detail;
87
+ }
88
+ };
89
+ var MCPConfigExtensionError = class extends Error {
90
+ path;
91
+ extension;
92
+ constructor(path, extension) {
93
+ super(`unsupported config file extension "${extension}" at ${path}: expected .json, .ts, .js, or .mjs`);
94
+ this.name = "MCPConfigExtensionError";
95
+ this.path = path;
96
+ this.extension = extension;
97
+ }
98
+ };
99
+ function validateEntry(entry, index, configPath) {
100
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
101
+ throw new MCPConfigParseError(configPath, `servers[${index}]: entry must be an object`);
102
+ }
103
+ const raw = entry;
104
+ const transport = raw["transport"] === void 0 ? "http" : raw["transport"];
105
+ if (transport !== "http" && transport !== "stdio") {
106
+ throw new MCPConfigParseError(configPath, `servers[${index}]: unrecognized transport value "${String(transport)}"`);
107
+ }
108
+ if (raw["prefix"] !== void 0 && typeof raw["prefix"] !== "string") {
109
+ throw new MCPConfigParseError(configPath, `servers[${index}]: prefix must be a string`);
110
+ }
111
+ if (raw["rediscover"] !== void 0 && raw["rediscover"] !== "per-session") {
112
+ throw new MCPConfigParseError(configPath, `servers[${index}]: rediscover must be "per-session"`);
113
+ }
114
+ const prefix = raw["prefix"];
115
+ const rediscover = raw["rediscover"];
116
+ if (transport === "http") {
117
+ if (!("url" in raw) || raw["url"] === void 0) {
118
+ throw new MCPConfigParseError(configPath, `servers[${index}]: http entry missing required field: url`);
119
+ }
120
+ if (typeof raw["url"] !== "string") {
121
+ throw new MCPConfigParseError(configPath, `servers[${index}]: url must be a string`);
122
+ }
123
+ const result2 = { url: raw["url"] };
124
+ if (prefix !== void 0) result2.prefix = prefix;
125
+ if (rediscover !== void 0) result2.rediscover = rediscover;
126
+ return result2;
127
+ }
128
+ if (!("command" in raw) || raw["command"] === void 0) {
129
+ throw new MCPConfigParseError(configPath, `servers[${index}]: stdio entry missing required field: command`);
130
+ }
131
+ if (typeof raw["command"] !== "string") {
132
+ throw new MCPConfigParseError(configPath, `servers[${index}]: command must be a string`);
133
+ }
134
+ if (raw["args"] !== void 0) {
135
+ if (!Array.isArray(raw["args"])) {
136
+ throw new MCPConfigParseError(configPath, `servers[${index}]: args must be a string array`);
137
+ }
138
+ if (!raw["args"].every((a) => typeof a === "string")) {
139
+ throw new MCPConfigParseError(configPath, `servers[${index}]: args must be a string array`);
140
+ }
141
+ }
142
+ if (raw["env"] !== void 0) {
143
+ const env = raw["env"];
144
+ if (typeof env !== "object" || env === null || Array.isArray(env)) {
145
+ throw new MCPConfigParseError(configPath, `servers[${index}]: env must be a plain object with string values`);
146
+ }
147
+ if (!Object.values(env).every((v) => typeof v === "string")) {
148
+ throw new MCPConfigParseError(configPath, `servers[${index}]: env must be a plain object with string values`);
149
+ }
150
+ }
151
+ const result = { command: raw["command"] };
152
+ if (raw["args"] !== void 0) result.args = raw["args"];
153
+ if (raw["env"] !== void 0) result.env = raw["env"];
154
+ if (prefix !== void 0) result.prefix = prefix;
155
+ if (rediscover !== void 0) result.rediscover = rediscover;
156
+ return result;
157
+ }
158
+ function validateServers(data, configPath) {
159
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
160
+ throw new MCPConfigParseError(configPath, "config must be a plain object");
161
+ }
162
+ const obj = data;
163
+ if (!Array.isArray(obj["servers"])) {
164
+ throw new MCPConfigParseError(configPath, "servers must be an array");
165
+ }
166
+ return obj["servers"].map((entry, i) => validateEntry(entry, i, configPath));
167
+ }
168
+ async function loadFromDynamicImport(resolvedPath, originalPath) {
169
+ const fileUrl = pathToFileURL(resolvedPath).href;
170
+ const mod = await import(fileUrl);
171
+ const defaultExport = mod.default;
172
+ if (typeof defaultExport !== "object" || defaultExport === null) {
173
+ throw new MCPConfigParseError(originalPath, "default export must be a non-null object (missing or invalid default export)");
174
+ }
175
+ return validateServers(defaultExport, originalPath);
176
+ }
177
+ function loadFromJson(resolvedPath, originalPath) {
178
+ let raw;
179
+ try {
180
+ raw = readFileSync(resolvedPath, "utf-8");
181
+ } catch (err) {
182
+ throw new MCPConfigParseError(originalPath, err.message);
183
+ }
184
+ let data;
185
+ try {
186
+ data = JSON.parse(raw);
187
+ } catch (err) {
188
+ throw new MCPConfigParseError(originalPath, err.message);
189
+ }
190
+ return validateServers(data, originalPath);
191
+ }
192
+ async function loadMCPConfig(configPath) {
193
+ const resolvedPath = resolve(configPath);
194
+ const ext = extname(configPath);
195
+ if (ext === ".json") {
196
+ return loadFromJson(resolvedPath, configPath);
197
+ }
198
+ if (ext === ".ts" || ext === ".js" || ext === ".mjs") {
199
+ return loadFromDynamicImport(resolvedPath, configPath);
200
+ }
201
+ throw new MCPConfigExtensionError(configPath, ext);
202
+ }
203
+
204
+ // src/mcp-manager.ts
205
+ var MCPServerNotFoundError = class extends Error {
206
+ key;
207
+ constructor(key) {
208
+ super(`no MCP server registered with key: ${key}`);
209
+ this.name = "MCPServerNotFoundError";
210
+ this.key = key;
211
+ }
212
+ };
213
+ var MCPManager = class _MCPManager {
214
+ clients;
215
+ options;
216
+ constructor(clients, options = {}) {
217
+ this.clients = [...clients];
218
+ this.options = options;
219
+ }
220
+ tools() {
221
+ const map = /* @__PURE__ */ new Map();
222
+ for (const client of this.clients) {
223
+ for (const tool of client.tools()) {
224
+ map.set(tool.name, tool);
225
+ }
226
+ }
227
+ return [...map.values()];
228
+ }
229
+ async addServer(params) {
230
+ let client;
231
+ if ("url" in params) {
232
+ const { url, ...clientOptions } = params;
233
+ client = await MCPClient.fromHttp(url, clientOptions);
234
+ } else {
235
+ client = await MCPClient.fromStdio(params);
236
+ }
237
+ this.clients.push(client);
238
+ }
239
+ async removeServer(key) {
240
+ const index = this.clients.findIndex((c) => c.url === key || c.command === key);
241
+ if (index === -1) {
242
+ throw new MCPServerNotFoundError(key);
243
+ }
244
+ const client = this.clients[index];
245
+ await client.disconnect();
246
+ this.clients.splice(index, 1);
247
+ }
248
+ bindObserver(_observer) {
249
+ const applicable = this.clients.filter((c) => this.shouldRediscover(c));
250
+ if (applicable.length > 0) {
251
+ void Promise.all(applicable.map((c) => c.discover()));
252
+ }
253
+ }
254
+ async loadConfig(path) {
255
+ const entries = await loadMCPConfig(path);
256
+ for (const entry of entries) {
257
+ await this.addServer(entry);
258
+ }
259
+ }
260
+ async watch(path, options) {
261
+ const resolvedPath = resolve2(path);
262
+ const onError = options?.onError;
263
+ let disposed = false;
264
+ let debounceTimer;
265
+ const reload = async () => {
266
+ let entries;
267
+ try {
268
+ entries = await loadMCPConfig(resolvedPath);
269
+ } catch (err) {
270
+ onError?.(err);
271
+ return;
272
+ }
273
+ const currentKeys = new Set(this.clients.map((c) => c.url ?? c.command ?? ""));
274
+ const newKeys = new Set(entries.map((e) => ("url" in e ? e.url : e.command) ?? ""));
275
+ const toAdd = entries.filter((e) => {
276
+ const key = ("url" in e ? e.url : e.command) ?? "";
277
+ if (!currentKeys.has(key)) return true;
278
+ const existing = this.clients.find((c) => (c.url ?? c.command) === key);
279
+ if (existing === void 0) return true;
280
+ const existingPrefix = existing.options.prefix;
281
+ const newPrefix = e.prefix;
282
+ return existingPrefix !== newPrefix;
283
+ });
284
+ const toAddKeys = new Set(toAdd.map((e) => ("url" in e ? e.url : e.command) ?? ""));
285
+ const toRemoveKeys = [...currentKeys].filter((k) => !newKeys.has(k) || toAddKeys.has(k));
286
+ const addedKeys = [];
287
+ try {
288
+ for (const entry of toAdd) {
289
+ await this.addServer(entry);
290
+ addedKeys.push(("url" in entry ? entry.url : entry.command) ?? "");
291
+ }
292
+ } catch (err) {
293
+ for (let i = addedKeys.length - 1; i >= 0; i--) {
294
+ try {
295
+ await this.removeServer(addedKeys[i]);
296
+ } catch {
297
+ }
298
+ }
299
+ onError?.(err);
300
+ return;
301
+ }
302
+ for (const key of toRemoveKeys) {
303
+ try {
304
+ await this.removeServer(key);
305
+ } catch (err) {
306
+ onError?.(err);
307
+ return;
308
+ }
309
+ }
310
+ };
311
+ const watcher = fsWatch(resolvedPath);
312
+ watcher.on("change", () => {
313
+ if (disposed) return;
314
+ clearTimeout(debounceTimer);
315
+ debounceTimer = setTimeout(() => {
316
+ void reload();
317
+ }, 100);
318
+ });
319
+ watcher.on("error", (err) => {
320
+ disposed = true;
321
+ clearTimeout(debounceTimer);
322
+ watcher.close();
323
+ onError?.(err);
324
+ });
325
+ return () => {
326
+ if (!disposed) {
327
+ disposed = true;
328
+ clearTimeout(debounceTimer);
329
+ watcher.close();
330
+ }
331
+ return Promise.resolve();
332
+ };
333
+ }
334
+ static async fromConfig(path) {
335
+ const manager = new _MCPManager([]);
336
+ await manager.loadConfig(path);
337
+ return manager;
338
+ }
339
+ shouldRediscover(client) {
340
+ if (client.options.rediscover === "per-session") return true;
341
+ if (client.options.rediscover === void 0 && this.options.rediscover === "per-session") return true;
342
+ return false;
343
+ }
344
+ };
345
+ export {
346
+ MCPClient,
347
+ MCPConfigExtensionError,
348
+ MCPConfigParseError,
349
+ MCPManager,
350
+ MCPNotConnectedError,
351
+ MCPServerNotFoundError
352
+ };
353
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp-client.ts","../src/mcp-manager.ts","../src/mcp-config-loader.ts"],"sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\"\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\"\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\"\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\"\nimport type { Tool } from \"@noetaris/harness-types\"\n\nexport interface MCPClientOptions {\n prefix?: string\n rediscover?: \"per-session\"\n}\n\nexport interface MCPStdioParams extends MCPClientOptions {\n command: string\n args?: string[]\n env?: Record<string, string>\n}\n\ntype TransportKind = \"http\" | \"stdio\"\n\nexport type MCPCallToolResult = {\n content: Array<{ type: string; text?: string; [key: string]: unknown }>\n isError?: boolean\n}\n\nexport class MCPNotConnectedError extends Error {\n constructor() {\n super(\"not connected: client has been disconnected\")\n this.name = \"MCPNotConnectedError\"\n }\n}\n\nexport class MCPClient {\n readonly url: string | undefined\n readonly command: string | undefined\n readonly options: MCPClientOptions\n readonly transportKind: TransportKind\n\n private sdk: Client\n private cachedTools: Tool[] = []\n private connected = true\n\n private constructor(\n transportKind: TransportKind,\n options: MCPClientOptions,\n url?: string,\n command?: string,\n ) {\n this.transportKind = transportKind\n this.options = options\n this.url = url\n this.command = command\n this.sdk = new Client({ name: \"@noetaris/harness-mcp\", version: \"0.1.0\" })\n }\n\n static async fromHttp(url: string, options: MCPClientOptions = {}): Promise<MCPClient> {\n const client = new MCPClient(\"http\", options, url, undefined)\n const transport = new StreamableHTTPClientTransport(new URL(url))\n await client.sdk.connect(transport as Transport) // as: StreamableHTTPClientTransport implements Transport but the SDK typings don't extend the base interface directly\n await client.discover()\n return client\n }\n\n static async fromStdio(params: MCPStdioParams): Promise<MCPClient> {\n const { command, args, env, ...options } = params\n const client = new MCPClient(\"stdio\", options, undefined, command)\n const stdioParams = { command, ...(args !== undefined && { args }), ...(env !== undefined && { env }) }\n const transport = new StdioClientTransport(stdioParams)\n await client.sdk.connect(transport as Transport) // as: StdioClientTransport implements Transport but the SDK typings don't extend the base interface directly\n await client.discover()\n return client\n }\n\n async discover(): Promise<void> {\n const result = await this.sdk.listTools()\n const prefix = this.options.prefix\n this.cachedTools = result.tools.map(t => ({\n name: prefix !== undefined ? `${prefix}/${t.name}` : t.name,\n description: t.description ?? \"\",\n inputSchema: t.inputSchema as Record<string, unknown>, // as: MCP SDK types inputSchema as a specific JSON Schema type; harness Tool uses the wider Record<string, unknown>\n }))\n }\n\n tools(): Tool[] {\n if (!this.connected) {\n throw new MCPNotConnectedError()\n }\n return this.cachedTools\n }\n\n async callTool(params: { name: string; arguments?: Record<string, unknown> }): Promise<MCPCallToolResult> {\n if (!this.connected) {\n throw new MCPNotConnectedError()\n }\n const result = await this.sdk.callTool(params)\n return result as MCPCallToolResult // as: SDK callTool returns a wider union type; MCPCallToolResult matches the content structure\n }\n\n async disconnect(): Promise<void> {\n this.connected = false\n await this.sdk.close()\n }\n}\n","import { watch as fsWatch } from 'node:fs'\nimport { resolve } from 'node:path'\nimport type { Tool } from '@noetaris/harness-types'\nimport { MCPClient, type MCPClientOptions, type MCPStdioParams } from './mcp-client.js'\nimport { loadMCPConfig } from './mcp-config-loader.js'\n\nexport interface MCPManagerOptions {\n rediscover?: 'per-session'\n}\n\nexport interface MCPWatchOptions {\n onError?: (err: Error) => void\n}\n\nexport interface MCPHttpParams extends MCPClientOptions {\n url: string\n}\n\ntype LocalObserver = {\n onRunStart?: (...args: unknown[]) => void\n onRunEnd?: (...args: unknown[]) => void\n onStepStart?: (...args: unknown[]) => void\n onStepEnd?: (...args: unknown[]) => void\n onStepError?: (...args: unknown[]) => void\n onInterrupt?: (...args: unknown[]) => void\n onEvent?: (...args: unknown[]) => void\n}\n\nexport class MCPServerNotFoundError extends Error {\n readonly key: string\n constructor(key: string) {\n super(`no MCP server registered with key: ${key}`)\n this.name = 'MCPServerNotFoundError'\n this.key = key\n }\n}\n\nexport class MCPManager {\n private clients: MCPClient[]\n private readonly options: MCPManagerOptions\n\n constructor(clients: MCPClient[], options: MCPManagerOptions = {}) {\n this.clients = [...clients]\n this.options = options\n }\n\n tools(): Tool[] {\n const map = new Map<string, Tool>()\n for (const client of this.clients) {\n for (const tool of client.tools()) {\n map.set(tool.name, tool)\n }\n }\n return [...map.values()]\n }\n\n async addServer(params: MCPHttpParams | MCPStdioParams): Promise<void> {\n let client: MCPClient\n if ('url' in params) {\n const { url, ...clientOptions } = params\n client = await MCPClient.fromHttp(url, clientOptions)\n } else {\n client = await MCPClient.fromStdio(params)\n }\n this.clients.push(client)\n }\n\n async removeServer(key: string): Promise<void> {\n const index = this.clients.findIndex(c => c.url === key || c.command === key)\n if (index === -1) {\n throw new MCPServerNotFoundError(key)\n }\n // noUncheckedIndexedAccess: index is validated above so the value is defined\n const client = this.clients[index]!\n await client.disconnect()\n this.clients.splice(index, 1)\n }\n\n bindObserver(_observer: LocalObserver): void {\n const applicable = this.clients.filter(c => this.shouldRediscover(c))\n if (applicable.length > 0) {\n void Promise.all(applicable.map(c => c.discover()))\n }\n }\n\n async loadConfig(path: string): Promise<void> {\n const entries = await loadMCPConfig(path)\n for (const entry of entries) {\n await this.addServer(entry)\n }\n }\n\n async watch(path: string, options?: MCPWatchOptions): Promise<() => Promise<void>> {\n const resolvedPath = resolve(path)\n const onError = options?.onError\n let disposed = false\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const reload = async (): Promise<void> => {\n let entries: Array<MCPHttpParams | MCPStdioParams>\n try {\n entries = await loadMCPConfig(resolvedPath)\n } catch (err) {\n onError?.(err as Error) // as: caught value is typed unknown; caller expects Error\n return\n }\n\n const currentKeys = new Set(this.clients.map(c => c.url ?? c.command ?? ''))\n const newKeys = new Set(entries.map(e => ('url' in e ? e.url : e.command) ?? ''))\n\n // entries whose key is not in current set, OR whose key is present but options differ\n const toAdd = entries.filter(e => {\n const key = ('url' in e ? e.url : e.command) ?? ''\n if (!currentKeys.has(key)) return true\n const existing = this.clients.find(c => (c.url ?? c.command) === key)\n if (existing === undefined) return true\n // compare prefix option: if different, treat as replace\n const existingPrefix = existing.options.prefix\n const newPrefix = e.prefix\n return existingPrefix !== newPrefix\n })\n // keys to remove: not in new set, OR same key but options differ (matched toAdd key)\n const toAddKeys = new Set(toAdd.map(e => ('url' in e ? e.url : e.command) ?? ''))\n const toRemoveKeys = [...currentKeys].filter(k => !newKeys.has(k) || toAddKeys.has(k))\n\n const addedKeys: string[] = []\n try {\n for (const entry of toAdd) {\n await this.addServer(entry)\n addedKeys.push(('url' in entry ? entry.url : entry.command) ?? '')\n }\n } catch (err) {\n // rollback successfully added servers in reverse order; swallow rollback errors to ensure onError is always called\n for (let i = addedKeys.length - 1; i >= 0; i--) {\n try { await this.removeServer(addedKeys[i]!) } catch { /* swallow: rollback best-effort; onError called below */ }\n }\n onError?.(err as Error) // as: caught value is typed unknown; caller expects Error\n return\n }\n\n for (const key of toRemoveKeys) {\n try {\n await this.removeServer(key)\n } catch (err) {\n onError?.(err as Error) // as: caught value is typed unknown; caller expects Error\n return\n }\n }\n }\n\n const watcher = fsWatch(resolvedPath)\n\n watcher.on('change', () => {\n if (disposed) return\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void reload()\n }, 100)\n })\n\n watcher.on('error', (err: Error) => {\n disposed = true\n clearTimeout(debounceTimer)\n watcher.close()\n onError?.(err)\n })\n\n return (): Promise<void> => {\n if (!disposed) {\n disposed = true\n clearTimeout(debounceTimer)\n watcher.close()\n }\n return Promise.resolve()\n }\n }\n\n static async fromConfig(path: string): Promise<MCPManager> {\n const manager = new MCPManager([])\n await manager.loadConfig(path)\n return manager\n }\n\n private shouldRediscover(client: MCPClient): boolean {\n if (client.options.rediscover === 'per-session') return true\n if (client.options.rediscover === undefined && this.options.rediscover === 'per-session') return true\n return false\n }\n}\n","import { readFileSync } from 'node:fs'\nimport { resolve, extname } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { MCPHttpParams } from './mcp-manager.js'\nimport type { MCPStdioParams } from './mcp-client.js'\n\nexport interface MCPHttpEntry {\n transport?: 'http'\n url: string\n prefix?: string\n rediscover?: 'per-session'\n}\n\nexport interface MCPStdioEntry {\n transport: 'stdio'\n command: string\n args?: string[]\n env?: Record<string, string>\n prefix?: string\n rediscover?: 'per-session'\n}\n\nexport type MCPServerEntry = MCPHttpEntry | MCPStdioEntry\n\nexport interface MCPConfigSchema {\n servers: MCPServerEntry[]\n}\n\nexport class MCPConfigParseError extends Error {\n readonly path: string\n readonly detail: string\n constructor(path: string, detail: string) {\n super(`invalid MCP config at ${path}: ${detail}`)\n this.name = 'MCPConfigParseError'\n this.path = path\n this.detail = detail\n }\n}\n\nexport class MCPConfigExtensionError extends Error {\n readonly path: string\n readonly extension: string\n constructor(path: string, extension: string) {\n super(`unsupported config file extension \"${extension}\" at ${path}: expected .json, .ts, .js, or .mjs`)\n this.name = 'MCPConfigExtensionError'\n this.path = path\n this.extension = extension\n }\n}\n\nfunction validateEntry(entry: unknown, index: number, configPath: string): MCPHttpParams | MCPStdioParams {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n throw new MCPConfigParseError(configPath, `servers[${index}]: entry must be an object`)\n }\n\n const raw = entry as Record<string, unknown> // as: entry is not-null object (Array check above); Record<string,unknown> is the narrowest safe widening\n\n // default transport to 'http' if absent\n const transport = raw['transport'] === undefined ? 'http' : raw['transport']\n\n if (transport !== 'http' && transport !== 'stdio') {\n throw new MCPConfigParseError(configPath, `servers[${index}]: unrecognized transport value \"${String(transport)}\"`)\n }\n\n // validate shared optional fields\n if (raw['prefix'] !== undefined && typeof raw['prefix'] !== 'string') {\n throw new MCPConfigParseError(configPath, `servers[${index}]: prefix must be a string`)\n }\n\n if (raw['rediscover'] !== undefined && raw['rediscover'] !== 'per-session') {\n throw new MCPConfigParseError(configPath, `servers[${index}]: rediscover must be \"per-session\"`)\n }\n\n const prefix = raw['prefix'] as string | undefined // as: validated typeof === 'string' in the guard above\n const rediscover = raw['rediscover'] as 'per-session' | undefined // as: validated === 'per-session' in the guard above\n\n if (transport === 'http') {\n if (!('url' in raw) || raw['url'] === undefined) {\n throw new MCPConfigParseError(configPath, `servers[${index}]: http entry missing required field: url`)\n }\n if (typeof raw['url'] !== 'string') {\n throw new MCPConfigParseError(configPath, `servers[${index}]: url must be a string`)\n }\n\n const result: MCPHttpParams = { url: raw['url'] }\n if (prefix !== undefined) result.prefix = prefix\n if (rediscover !== undefined) result.rediscover = rediscover\n return result\n }\n\n // stdio\n if (!('command' in raw) || raw['command'] === undefined) {\n throw new MCPConfigParseError(configPath, `servers[${index}]: stdio entry missing required field: command`)\n }\n if (typeof raw['command'] !== 'string') {\n throw new MCPConfigParseError(configPath, `servers[${index}]: command must be a string`)\n }\n\n if (raw['args'] !== undefined) {\n if (!Array.isArray(raw['args'])) {\n throw new MCPConfigParseError(configPath, `servers[${index}]: args must be a string array`)\n }\n if (!(raw['args'] as unknown[]).every(a => typeof a === 'string')) { // as: Array.isArray confirmed above; unknown[] is safe for .every narrowing\n throw new MCPConfigParseError(configPath, `servers[${index}]: args must be a string array`)\n }\n }\n\n if (raw['env'] !== undefined) {\n const env = raw['env']\n if (typeof env !== 'object' || env === null || Array.isArray(env)) {\n throw new MCPConfigParseError(configPath, `servers[${index}]: env must be a plain object with string values`)\n }\n if (!Object.values(env as Record<string, unknown>).every(v => typeof v === 'string')) { // as: object/non-null/non-array confirmed above; Record<string,unknown> for Object.values narrowing\n throw new MCPConfigParseError(configPath, `servers[${index}]: env must be a plain object with string values`)\n }\n }\n\n const result: MCPStdioParams = { command: raw['command'] }\n if (raw['args'] !== undefined) result.args = raw['args'] as string[] // as: validated Array.isArray + every element is string above\n if (raw['env'] !== undefined) result.env = raw['env'] as Record<string, string> // as: validated plain object with all-string values above\n if (prefix !== undefined) result.prefix = prefix\n if (rediscover !== undefined) result.rediscover = rediscover\n return result\n}\n\nfunction validateServers(data: unknown, configPath: string): Array<MCPHttpParams | MCPStdioParams> {\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n throw new MCPConfigParseError(configPath, 'config must be a plain object')\n }\n\n const obj = data as Record<string, unknown> // as: data is non-null non-array object (guards above); Record<string,unknown> is the narrowest safe widening\n\n if (!Array.isArray(obj['servers'])) {\n throw new MCPConfigParseError(configPath, 'servers must be an array')\n }\n\n return (obj['servers'] as unknown[]).map((entry, i) => validateEntry(entry, i, configPath)) // as: Array.isArray confirmed on line above; unknown[] is the safe element type for map\n}\n\nasync function loadFromDynamicImport(resolvedPath: string, originalPath: string): Promise<Array<MCPHttpParams | MCPStdioParams>> {\n const fileUrl = pathToFileURL(resolvedPath).href\n // dynamic import errors (missing file, syntax error) propagate as-is\n const mod = await import(fileUrl)\n const defaultExport: unknown = mod.default\n\n if (typeof defaultExport !== 'object' || defaultExport === null) {\n throw new MCPConfigParseError(originalPath, 'default export must be a non-null object (missing or invalid default export)')\n }\n\n return validateServers(defaultExport, originalPath)\n}\n\nfunction loadFromJson(resolvedPath: string, originalPath: string): Array<MCPHttpParams | MCPStdioParams> {\n let raw: string\n try {\n raw = readFileSync(resolvedPath, 'utf-8')\n } catch (err) {\n throw new MCPConfigParseError(originalPath, (err as Error).message) // as: catch binds unknown; fs errors are always Error instances with .message\n }\n\n let data: unknown\n try {\n data = JSON.parse(raw)\n } catch (err) {\n throw new MCPConfigParseError(originalPath, (err as Error).message) // as: catch binds unknown; JSON.parse throws SyntaxError (an Error) with .message\n }\n\n return validateServers(data, originalPath)\n}\n\nexport async function loadMCPConfig(configPath: string): Promise<Array<MCPHttpParams | MCPStdioParams>> {\n const resolvedPath = resolve(configPath)\n const ext = extname(configPath)\n\n if (ext === '.json') {\n return loadFromJson(resolvedPath, configPath)\n }\n\n if (ext === '.ts' || ext === '.js' || ext === '.mjs') {\n return loadFromDynamicImport(resolvedPath, configPath)\n }\n\n throw new MCPConfigExtensionError(configPath, ext)\n}\n"],"mappings":"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","names":["resolve","result","resolve"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@noetaris/harness-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server integration for @noetaris/harness",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/noetaris-lab/harness-mcp.git"
9
+ },
10
+ "homepage": "https://github.com/noetaris-lab/harness-mcp#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/noetaris-lab/harness-mcp/issues"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "prepublishOnly": "pnpm build && pnpm test",
31
+ "publish:npm": "pnpm publish --access public --no-git-checks"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "pnpm": {
37
+ "onlyBuiltDependencies": [
38
+ "esbuild"
39
+ ]
40
+ },
41
+ "devDependencies": {
42
+ "@noetaris/harness-types": "^0.2.0",
43
+ "@types/node": "^25.9.1",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^4.1.7"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.29.0"
50
+ },
51
+ "peerDependencies": null
52
+ }