@zhijiewang/openharness 2.10.0 → 2.12.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 +14 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/info.js +49 -7
- package/dist/commands/mcp-auth.d.ts +11 -0
- package/dist/commands/mcp-auth.js +57 -0
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/REPL.js +10 -3
- package/dist/harness/config.d.ts +19 -3
- package/dist/harness/submit-handler.js +1 -1
- package/dist/mcp/client.d.ts +20 -10
- package/dist/mcp/client.js +89 -109
- package/dist/mcp/config-normalize.d.ts +24 -0
- package/dist/mcp/config-normalize.js +72 -0
- package/dist/mcp/loader.js +24 -0
- package/dist/mcp/oauth-storage.d.ts +23 -0
- package/dist/mcp/oauth-storage.js +58 -0
- package/dist/mcp/oauth.d.ts +79 -0
- package/dist/mcp/oauth.js +257 -0
- package/dist/mcp/transport.d.ts +49 -0
- package/dist/mcp/transport.js +219 -0
- package/dist/mcp/types.d.ts +1 -16
- package/dist/mcp/types.js +0 -1
- package/dist/tools/TaskUpdateTool/index.d.ts +2 -2
- package/package.json +3 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
const pkg = createRequire(import.meta.url)("../../package.json");
|
|
8
|
+
export class RemoteAuthRequiredError extends Error {
|
|
9
|
+
serverName;
|
|
10
|
+
wwwAuthenticate;
|
|
11
|
+
constructor(serverName, wwwAuthenticate) {
|
|
12
|
+
super(`MCP server '${serverName}' requires authentication. ` +
|
|
13
|
+
`Add 'auth: oauth' to enable the OAuth 2.1 flow, or set headers.Authorization for a static bearer token.`);
|
|
14
|
+
this.name = "RemoteAuthRequiredError";
|
|
15
|
+
this.serverName = serverName;
|
|
16
|
+
this.wwwAuthenticate = wwwAuthenticate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class UnreachableError extends Error {
|
|
20
|
+
serverName;
|
|
21
|
+
cause;
|
|
22
|
+
constructor(serverName, cause) {
|
|
23
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
24
|
+
super(`MCP server '${serverName}' unreachable: ${causeMsg}`);
|
|
25
|
+
this.name = "UnreachableError";
|
|
26
|
+
this.serverName = serverName;
|
|
27
|
+
this.cause = cause;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ProtocolError extends Error {
|
|
31
|
+
serverName;
|
|
32
|
+
cause;
|
|
33
|
+
constructor(serverName, cause) {
|
|
34
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
35
|
+
super(`MCP server '${serverName}' protocol error: ${causeMsg}`);
|
|
36
|
+
this.name = "ProtocolError";
|
|
37
|
+
this.serverName = serverName;
|
|
38
|
+
this.cause = cause;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Construct an SDK Transport for a normalized config.
|
|
43
|
+
* Does NOT call .start() — caller (Client.connect) handles that.
|
|
44
|
+
*/
|
|
45
|
+
export async function buildTransport(cfg, opts = {}) {
|
|
46
|
+
if (cfg.type === "stdio") {
|
|
47
|
+
return new StdioClientTransport({
|
|
48
|
+
command: cfg.command,
|
|
49
|
+
args: cfg.args,
|
|
50
|
+
env: cfg.env,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (cfg.type === "http") {
|
|
54
|
+
return new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
55
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
56
|
+
authProvider: opts.authProvider,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (cfg.type === "sse") {
|
|
60
|
+
return new SSEClientTransport(new URL(cfg.url), {
|
|
61
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
62
|
+
authProvider: opts.authProvider,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`unknown transport type: ${cfg.type}`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Does this error indicate "try the legacy SSE transport instead"?
|
|
69
|
+
* Yes for 4xx OTHER than 401-with-WWW-Authenticate (which is a real auth challenge).
|
|
70
|
+
*/
|
|
71
|
+
function isFallbackCandidate(err) {
|
|
72
|
+
const code = err?.code;
|
|
73
|
+
if (typeof code !== "number")
|
|
74
|
+
return false;
|
|
75
|
+
if (code < 400 || code >= 500)
|
|
76
|
+
return false;
|
|
77
|
+
if (code === 401) {
|
|
78
|
+
const www = err?.headers?.["www-authenticate"];
|
|
79
|
+
if (typeof www === "string" && www.length > 0)
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function extractWwwAuthenticate(err) {
|
|
85
|
+
const code = err?.code;
|
|
86
|
+
if (code !== 401)
|
|
87
|
+
return undefined;
|
|
88
|
+
const www = err?.headers?.["www-authenticate"];
|
|
89
|
+
return typeof www === "string" ? www : undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Connect to an MCP server, with auto-fallback from Streamable HTTP to
|
|
93
|
+
* legacy SSE when the config's type was INFERRED from url (not explicit).
|
|
94
|
+
*
|
|
95
|
+
* `doConnect` is the side-effecting step — build/connect a transport and
|
|
96
|
+
* return the opaque client. Kept injectable for tests; production call-site
|
|
97
|
+
* wires it to `buildClient` (Task 7).
|
|
98
|
+
*/
|
|
99
|
+
export async function connectWithFallback(cfg, doConnect) {
|
|
100
|
+
try {
|
|
101
|
+
return await doConnect(cfg);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
// Auth challenge → surface immediately, never fall back
|
|
105
|
+
const www = extractWwwAuthenticate(err);
|
|
106
|
+
if (www !== undefined)
|
|
107
|
+
throw new RemoteAuthRequiredError(cfg.name, www);
|
|
108
|
+
// Explicit type → surface as-is
|
|
109
|
+
const inferred = cfg.inferredFromUrl === true;
|
|
110
|
+
if (!inferred)
|
|
111
|
+
throw err;
|
|
112
|
+
// Only http-was-inferred-first falls back to sse; other shapes surface
|
|
113
|
+
if (cfg.type !== "http")
|
|
114
|
+
throw err;
|
|
115
|
+
if (!isFallbackCandidate(err))
|
|
116
|
+
throw err;
|
|
117
|
+
// Log + retry
|
|
118
|
+
console.warn(`[mcp] ${cfg.name}: Streamable HTTP failed (${err.message}); trying legacy SSE`);
|
|
119
|
+
const sseCfg = { ...cfg, type: "sse" };
|
|
120
|
+
return await doConnect(sseCfg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
124
|
+
const CLIENT_INFO = { name: "openharness", version: pkg.version };
|
|
125
|
+
/** Duck-type check: does this provider expose awaitCallback (our OhOAuthProvider)? */
|
|
126
|
+
function hasAwaitCallback(p) {
|
|
127
|
+
return typeof p.awaitCallback === "function";
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Build a connected SDK Client for a normalized config.
|
|
131
|
+
* Maps connect-time errors into OH's typed error taxonomy.
|
|
132
|
+
*
|
|
133
|
+
* When the auth provider exposes `awaitCallback()` (i.e. OhOAuthProvider), this
|
|
134
|
+
* function handles the full OAuth callback → finishAuth → reconnect loop so callers
|
|
135
|
+
* don't need to orchestrate it manually.
|
|
136
|
+
*/
|
|
137
|
+
export async function buildClient(cfg, opts = {}) {
|
|
138
|
+
const transport = await buildTransport(cfg, opts);
|
|
139
|
+
const client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
140
|
+
const timeoutMs = cfg.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
141
|
+
async function tryConnect() {
|
|
142
|
+
let timer = null;
|
|
143
|
+
try {
|
|
144
|
+
await Promise.race([
|
|
145
|
+
client.connect(transport),
|
|
146
|
+
new Promise((_, reject) => {
|
|
147
|
+
timer = setTimeout(() => reject(new Error(`init timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (timer !== null)
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await tryConnect();
|
|
158
|
+
return client;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
// If the SDK requires a browser-based OAuth flow (UnauthorizedError after REDIRECT),
|
|
162
|
+
// and our provider knows how to await the callback, complete the loop here.
|
|
163
|
+
// Per the SDK design, after finishAuth we must create a fresh transport + client
|
|
164
|
+
// because the original transport is already in a "started" state.
|
|
165
|
+
if (err instanceof UnauthorizedError && opts.authProvider && hasAwaitCallback(opts.authProvider)) {
|
|
166
|
+
try {
|
|
167
|
+
const { code } = await opts.authProvider.awaitCallback();
|
|
168
|
+
await transport.finishAuth(code);
|
|
169
|
+
// Close the old transport before constructing a fresh one — the SDK's
|
|
170
|
+
// Transport is one-shot after an UnauthorizedError; leaving it open leaks
|
|
171
|
+
// the underlying TCP socket / event stream.
|
|
172
|
+
try {
|
|
173
|
+
await transport.close?.();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// best-effort
|
|
177
|
+
}
|
|
178
|
+
// Build a fresh transport + client for the authenticated retry
|
|
179
|
+
const freshTransport = await buildTransport(cfg, opts);
|
|
180
|
+
const freshClient = new Client(CLIENT_INFO, { capabilities: {} });
|
|
181
|
+
let freshTimer = null;
|
|
182
|
+
try {
|
|
183
|
+
await Promise.race([
|
|
184
|
+
freshClient.connect(freshTransport),
|
|
185
|
+
new Promise((_, reject) => {
|
|
186
|
+
freshTimer = setTimeout(() => reject(new Error(`init timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
187
|
+
}),
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (freshTimer !== null)
|
|
192
|
+
clearTimeout(freshTimer);
|
|
193
|
+
}
|
|
194
|
+
return freshClient;
|
|
195
|
+
}
|
|
196
|
+
catch (oauthErr) {
|
|
197
|
+
// Classify the retry error the same way as the primary path
|
|
198
|
+
if (oauthErr instanceof RemoteAuthRequiredError ||
|
|
199
|
+
oauthErr instanceof UnreachableError ||
|
|
200
|
+
oauthErr instanceof ProtocolError) {
|
|
201
|
+
throw oauthErr;
|
|
202
|
+
}
|
|
203
|
+
throw new ProtocolError(cfg.name, oauthErr);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Leave RemoteAuthRequiredError / UnreachableError / ProtocolError as-is
|
|
207
|
+
if (err instanceof RemoteAuthRequiredError || err instanceof UnreachableError || err instanceof ProtocolError) {
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
// Network-shaped errors (DNS, TCP, TLS, timeout) → Unreachable
|
|
211
|
+
const msg = err?.message ?? String(err);
|
|
212
|
+
if (/timeout|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
|
|
213
|
+
throw new UnreachableError(cfg.name, err);
|
|
214
|
+
}
|
|
215
|
+
// Otherwise protocol-shaped
|
|
216
|
+
throw new ProtocolError(cfg.name, err);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=transport.js.map
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export interface JsonRpcRequest {
|
|
3
|
-
jsonrpc: "2.0";
|
|
4
|
-
id: number;
|
|
5
|
-
method: string;
|
|
6
|
-
params?: unknown;
|
|
7
|
-
}
|
|
8
|
-
export interface JsonRpcResponse {
|
|
9
|
-
jsonrpc: "2.0";
|
|
10
|
-
id: number;
|
|
11
|
-
result?: unknown;
|
|
12
|
-
error?: {
|
|
13
|
-
code: number;
|
|
14
|
-
message: string;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
1
|
+
/** MCP tool definition as returned by `tools/list`. */
|
|
17
2
|
export interface McpToolDef {
|
|
18
3
|
name: string;
|
|
19
4
|
description?: string;
|
package/dist/mcp/types.js
CHANGED
|
@@ -12,7 +12,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
12
12
|
addBlockedBy: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
14
|
taskId: number;
|
|
15
|
-
status?: "completed" | "
|
|
15
|
+
status?: "completed" | "cancelled" | "pending" | "in_progress" | "deleted" | undefined;
|
|
16
16
|
description?: string | undefined;
|
|
17
17
|
metadata?: Record<string, unknown> | undefined;
|
|
18
18
|
subject?: string | undefined;
|
|
@@ -22,7 +22,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
22
22
|
addBlockedBy?: number[] | undefined;
|
|
23
23
|
}, {
|
|
24
24
|
taskId: number;
|
|
25
|
-
status?: "completed" | "
|
|
25
|
+
status?: "completed" | "cancelled" | "pending" | "in_progress" | "deleted" | undefined;
|
|
26
26
|
description?: string | undefined;
|
|
27
27
|
metadata?: Record<string, unknown> | undefined;
|
|
28
28
|
subject?: string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhijiewang/openharness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Open-source terminal coding agent. Works with any LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"start": "node dist/main.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
40
|
"better-sqlite3": "^12.9.0",
|
|
40
41
|
"chalk": "^5.4.1",
|
|
41
42
|
"commander": "^13.0.0",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"ink-spinner": "^5.0.0",
|
|
44
45
|
"ink-text-input": "^6.0.0",
|
|
45
46
|
"marked": "^17.0.5",
|
|
47
|
+
"open": "^11.0.0",
|
|
46
48
|
"react": "^18.3.1",
|
|
47
49
|
"yaml": "^2.7.0",
|
|
48
50
|
"zod": "^3.24.0"
|