local-browser-bridge 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 +724 -0
- package/dist/package.json +61 -0
- package/dist/src/browser/chrome.d.ts +19 -0
- package/dist/src/browser/chrome.js +778 -0
- package/dist/src/browser/index.d.ts +3 -0
- package/dist/src/browser/index.js +25 -0
- package/dist/src/browser/safari.d.ts +41 -0
- package/dist/src/browser/safari.js +827 -0
- package/dist/src/browser-attach-ux-helper.d.ts +39 -0
- package/dist/src/browser-attach-ux-helper.js +157 -0
- package/dist/src/capabilities.d.ts +3 -0
- package/dist/src/capabilities.js +182 -0
- package/dist/src/chrome-relay-error-helper.d.ts +19 -0
- package/dist/src/chrome-relay-error-helper.js +78 -0
- package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
- package/dist/src/chrome-relay-helper-cli.js +97 -0
- package/dist/src/chrome-relay-helper.d.ts +29 -0
- package/dist/src/chrome-relay-helper.js +151 -0
- package/dist/src/chrome-relay-state.d.ts +23 -0
- package/dist/src/chrome-relay-state.js +108 -0
- package/dist/src/claude-code.d.ts +20 -0
- package/dist/src/claude-code.js +66 -0
- package/dist/src/cli-reference-adapter.d.ts +13 -0
- package/dist/src/cli-reference-adapter.js +48 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +200 -0
- package/dist/src/codex.d.ts +17 -0
- package/dist/src/codex.js +25 -0
- package/dist/src/connection-ux.d.ts +61 -0
- package/dist/src/connection-ux.js +256 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +58 -0
- package/dist/src/http-reference-adapter.d.ts +34 -0
- package/dist/src/http-reference-adapter.js +61 -0
- package/dist/src/http.d.ts +3 -0
- package/dist/src/http.js +161 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +43 -0
- package/dist/src/mcp-stdio.d.ts +2 -0
- package/dist/src/mcp-stdio.js +10 -0
- package/dist/src/mcp.d.ts +25 -0
- package/dist/src/mcp.js +483 -0
- package/dist/src/reference-adapter.d.ts +32 -0
- package/dist/src/reference-adapter.js +42 -0
- package/dist/src/service/attach-service.d.ts +28 -0
- package/dist/src/service/attach-service.js +272 -0
- package/dist/src/session-metadata.d.ts +4 -0
- package/dist/src/session-metadata.js +88 -0
- package/dist/src/store/session-store.d.ts +14 -0
- package/dist/src/store/session-store.js +52 -0
- package/dist/src/target.d.ts +9 -0
- package/dist/src/target.js +61 -0
- package/dist/src/types.d.ts +397 -0
- package/dist/src/types.js +2 -0
- package/dist/tests/attach-service.test.d.ts +1 -0
- package/dist/tests/attach-service.test.js +1367 -0
- package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
- package/dist/tests/browser-attach-ux-helper.test.js +139 -0
- package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-error-helper.test.js +67 -0
- package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-helper.test.js +142 -0
- package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
- package/dist/tests/chrome-relay-state-schema.test.js +96 -0
- package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
- package/dist/tests/claude-code-wrapper.test.js +170 -0
- package/dist/tests/codex.test.d.ts +1 -0
- package/dist/tests/codex.test.js +210 -0
- package/dist/tests/demo-client-smoke.test.d.ts +1 -0
- package/dist/tests/demo-client-smoke.test.js +405 -0
- package/dist/tests/docs-fixtures.test.d.ts +1 -0
- package/dist/tests/docs-fixtures.test.js +255 -0
- package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
- package/dist/tests/doctor-connect-wrapper.test.js +62 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
- package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
- package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
- package/dist/tests/fixtures/public-root-consumer.js +67 -0
- package/dist/tests/mcp.test.d.ts +1 -0
- package/dist/tests/mcp.test.js +345 -0
- package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
- package/dist/tests/public-consumer-helpers.test.js +33 -0
- package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
- package/dist/tests/public-package-git-consumption.test.js +56 -0
- package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
- package/dist/tests/public-root-consumer-smoke.test.js +214 -0
- package/dist/tests/reference-adapter.test.d.ts +1 -0
- package/dist/tests/reference-adapter.test.js +220 -0
- package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
- package/dist/tests/transport-reference-adapters.test.js +214 -0
- package/package.json +61 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable consumer-facing entrypoint for reusable local-browser-bridge helpers.
|
|
3
|
+
*
|
|
4
|
+
* These exports stay transport-neutral and agent-agnostic so local clients can
|
|
5
|
+
* consume the documented attach/resume and relay-failure interpretation helpers
|
|
6
|
+
* without reaching into incidental sample code paths.
|
|
7
|
+
*/
|
|
8
|
+
export { connectConnectionRoute, createServiceBridgeAdapter, doctorConnectionRoute, normalizeConnectionRouteName, type ConnectionCategory, type ConnectionConnectEnvelope, type ConnectionDoctorEnvelope, type ConnectionNextStep, type ConnectionOutcome, type ConnectionReason, type ConnectionRouteInput, type ConnectionRouteName, type ConnectionStatus } from "./connection-ux";
|
|
9
|
+
export { chromeRelayBranchPrompt, chromeRelayRetryGuidance, chromeRelayScopeNote, interpretChromeRelayFailure, type ChromeRelayFailureCategory, type ChromeRelayFailureInterpretation } from "./chrome-relay-error-helper";
|
|
10
|
+
export { interpretBrowserAttachUxFromDiagnostics, interpretBrowserAttachUxFromError, interpretBrowserAttachUxFromSession, type BrowserAttachUxInterpretation, type BrowserAttachUxState } from "./browser-attach-ux-helper";
|
|
11
|
+
export { connectViaBridge, createBridgeAdapter, sessionFromBridgeResult, type BridgeAdapter, type BridgeAttachRoute, type BridgeConnectionResult, type BridgeResumeRoute, type BridgeRoute, type BridgeSessionResult } from "./reference-adapter";
|
|
12
|
+
export { connectCodexViaCli, connectCodexViaHttp, normalizeCodexRoute, type CodexRouteInput, type CodexRouteName, type ConnectCodexViaCliOptions, type ConnectCodexViaHttpOptions } from "./codex";
|
|
13
|
+
export { createHttpBridgeAdapter, type CreateHttpBridgeAdapterOptions, type HttpAttachEnvelope, type HttpBridgeExecutor, type HttpBridgeRequest, type HttpBridgeResponse, type HttpCapabilitiesEnvelope, type HttpDiagnosticsEnvelope, type HttpResumeEnvelope } from "./http-reference-adapter";
|
|
14
|
+
export { createCliBridgeAdapter, type CliBridgeCommand, type CliBridgeCommandResult, type CliBridgeExecutor, type CreateCliBridgeAdapterOptions } from "./cli-reference-adapter";
|
|
15
|
+
export { normalizeClaudeCodeRoute, prepareClaudeCodeRoute, type ClaudeCodePreparedRoute, type ClaudeCodeRouteInput, type ClaudeCodeRouteName } from "./claude-code";
|
|
16
|
+
export { createMcpServer, runMcpStdioServer, SUPPORTED_PROTOCOL_VERSIONS } from "./mcp";
|
|
17
|
+
export type { AttachmentSession, BrowserAttachMode, BrowserAttachReadinessState, BrowserDiagnostics, ChromeRelayErrorDetails, ChromeRelayFailureBranch, ChromeRelayFailureOperation, ChromeRelaySharedTabScope, SupportedBrowser } from "./types";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_PROTOCOL_VERSIONS = exports.runMcpStdioServer = exports.createMcpServer = exports.prepareClaudeCodeRoute = exports.normalizeClaudeCodeRoute = exports.createCliBridgeAdapter = exports.createHttpBridgeAdapter = exports.normalizeCodexRoute = exports.connectCodexViaHttp = exports.connectCodexViaCli = exports.sessionFromBridgeResult = exports.createBridgeAdapter = exports.connectViaBridge = exports.interpretBrowserAttachUxFromSession = exports.interpretBrowserAttachUxFromError = exports.interpretBrowserAttachUxFromDiagnostics = exports.interpretChromeRelayFailure = exports.chromeRelayScopeNote = exports.chromeRelayRetryGuidance = exports.chromeRelayBranchPrompt = exports.normalizeConnectionRouteName = exports.doctorConnectionRoute = exports.createServiceBridgeAdapter = exports.connectConnectionRoute = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Stable consumer-facing entrypoint for reusable local-browser-bridge helpers.
|
|
6
|
+
*
|
|
7
|
+
* These exports stay transport-neutral and agent-agnostic so local clients can
|
|
8
|
+
* consume the documented attach/resume and relay-failure interpretation helpers
|
|
9
|
+
* without reaching into incidental sample code paths.
|
|
10
|
+
*/
|
|
11
|
+
var connection_ux_1 = require("./connection-ux");
|
|
12
|
+
Object.defineProperty(exports, "connectConnectionRoute", { enumerable: true, get: function () { return connection_ux_1.connectConnectionRoute; } });
|
|
13
|
+
Object.defineProperty(exports, "createServiceBridgeAdapter", { enumerable: true, get: function () { return connection_ux_1.createServiceBridgeAdapter; } });
|
|
14
|
+
Object.defineProperty(exports, "doctorConnectionRoute", { enumerable: true, get: function () { return connection_ux_1.doctorConnectionRoute; } });
|
|
15
|
+
Object.defineProperty(exports, "normalizeConnectionRouteName", { enumerable: true, get: function () { return connection_ux_1.normalizeConnectionRouteName; } });
|
|
16
|
+
var chrome_relay_error_helper_1 = require("./chrome-relay-error-helper");
|
|
17
|
+
Object.defineProperty(exports, "chromeRelayBranchPrompt", { enumerable: true, get: function () { return chrome_relay_error_helper_1.chromeRelayBranchPrompt; } });
|
|
18
|
+
Object.defineProperty(exports, "chromeRelayRetryGuidance", { enumerable: true, get: function () { return chrome_relay_error_helper_1.chromeRelayRetryGuidance; } });
|
|
19
|
+
Object.defineProperty(exports, "chromeRelayScopeNote", { enumerable: true, get: function () { return chrome_relay_error_helper_1.chromeRelayScopeNote; } });
|
|
20
|
+
Object.defineProperty(exports, "interpretChromeRelayFailure", { enumerable: true, get: function () { return chrome_relay_error_helper_1.interpretChromeRelayFailure; } });
|
|
21
|
+
var browser_attach_ux_helper_1 = require("./browser-attach-ux-helper");
|
|
22
|
+
Object.defineProperty(exports, "interpretBrowserAttachUxFromDiagnostics", { enumerable: true, get: function () { return browser_attach_ux_helper_1.interpretBrowserAttachUxFromDiagnostics; } });
|
|
23
|
+
Object.defineProperty(exports, "interpretBrowserAttachUxFromError", { enumerable: true, get: function () { return browser_attach_ux_helper_1.interpretBrowserAttachUxFromError; } });
|
|
24
|
+
Object.defineProperty(exports, "interpretBrowserAttachUxFromSession", { enumerable: true, get: function () { return browser_attach_ux_helper_1.interpretBrowserAttachUxFromSession; } });
|
|
25
|
+
var reference_adapter_1 = require("./reference-adapter");
|
|
26
|
+
Object.defineProperty(exports, "connectViaBridge", { enumerable: true, get: function () { return reference_adapter_1.connectViaBridge; } });
|
|
27
|
+
Object.defineProperty(exports, "createBridgeAdapter", { enumerable: true, get: function () { return reference_adapter_1.createBridgeAdapter; } });
|
|
28
|
+
Object.defineProperty(exports, "sessionFromBridgeResult", { enumerable: true, get: function () { return reference_adapter_1.sessionFromBridgeResult; } });
|
|
29
|
+
var codex_1 = require("./codex");
|
|
30
|
+
Object.defineProperty(exports, "connectCodexViaCli", { enumerable: true, get: function () { return codex_1.connectCodexViaCli; } });
|
|
31
|
+
Object.defineProperty(exports, "connectCodexViaHttp", { enumerable: true, get: function () { return codex_1.connectCodexViaHttp; } });
|
|
32
|
+
Object.defineProperty(exports, "normalizeCodexRoute", { enumerable: true, get: function () { return codex_1.normalizeCodexRoute; } });
|
|
33
|
+
var http_reference_adapter_1 = require("./http-reference-adapter");
|
|
34
|
+
Object.defineProperty(exports, "createHttpBridgeAdapter", { enumerable: true, get: function () { return http_reference_adapter_1.createHttpBridgeAdapter; } });
|
|
35
|
+
var cli_reference_adapter_1 = require("./cli-reference-adapter");
|
|
36
|
+
Object.defineProperty(exports, "createCliBridgeAdapter", { enumerable: true, get: function () { return cli_reference_adapter_1.createCliBridgeAdapter; } });
|
|
37
|
+
var claude_code_1 = require("./claude-code");
|
|
38
|
+
Object.defineProperty(exports, "normalizeClaudeCodeRoute", { enumerable: true, get: function () { return claude_code_1.normalizeClaudeCodeRoute; } });
|
|
39
|
+
Object.defineProperty(exports, "prepareClaudeCodeRoute", { enumerable: true, get: function () { return claude_code_1.prepareClaudeCodeRoute; } });
|
|
40
|
+
var mcp_1 = require("./mcp");
|
|
41
|
+
Object.defineProperty(exports, "createMcpServer", { enumerable: true, get: function () { return mcp_1.createMcpServer; } });
|
|
42
|
+
Object.defineProperty(exports, "runMcpStdioServer", { enumerable: true, get: function () { return mcp_1.runMcpStdioServer; } });
|
|
43
|
+
Object.defineProperty(exports, "SUPPORTED_PROTOCOL_VERSIONS", { enumerable: true, get: function () { return mcp_1.SUPPORTED_PROTOCOL_VERSIONS; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AttachService } from "./service/attach-service";
|
|
2
|
+
declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
3
|
+
type JsonRpcId = string | number | null;
|
|
4
|
+
interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: "2.0";
|
|
6
|
+
id: JsonRpcId;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface JsonRpcNotification {
|
|
11
|
+
jsonrpc: "2.0";
|
|
12
|
+
method: string;
|
|
13
|
+
params?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification;
|
|
16
|
+
export declare function createMcpServer(service?: Pick<AttachService, "getCapabilities" | "diagnostics" | "attach" | "resumeSession" | "listTabs">): {
|
|
17
|
+
handleMessage(message: JsonRpcMessage): Promise<unknown | undefined>;
|
|
18
|
+
};
|
|
19
|
+
export declare function runMcpStdioServer(options?: {
|
|
20
|
+
service?: Pick<AttachService, "getCapabilities" | "diagnostics" | "attach" | "resumeSession" | "listTabs">;
|
|
21
|
+
input?: NodeJS.ReadableStream;
|
|
22
|
+
output?: NodeJS.WritableStream;
|
|
23
|
+
error?: NodeJS.WritableStream;
|
|
24
|
+
}): void;
|
|
25
|
+
export { SUPPORTED_PROTOCOL_VERSIONS };
|
package/dist/src/mcp.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_PROTOCOL_VERSIONS = void 0;
|
|
4
|
+
exports.createMcpServer = createMcpServer;
|
|
5
|
+
exports.runMcpStdioServer = runMcpStdioServer;
|
|
6
|
+
const node_readline_1 = require("node:readline");
|
|
7
|
+
const connection_ux_1 = require("./connection-ux");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const attach_service_1 = require("./service/attach-service");
|
|
10
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
11
|
+
exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
|
|
12
|
+
const SERVER_NAME = "local-browser-bridge";
|
|
13
|
+
function isRequest(message) {
|
|
14
|
+
return "id" in message;
|
|
15
|
+
}
|
|
16
|
+
function isJsonRpcMessage(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && value.jsonrpc === "2.0"
|
|
18
|
+
&& typeof value.method === "string";
|
|
19
|
+
}
|
|
20
|
+
function negotiateProtocolVersion(requested) {
|
|
21
|
+
if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
|
|
22
|
+
return requested;
|
|
23
|
+
}
|
|
24
|
+
return SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
25
|
+
}
|
|
26
|
+
function writeMessage(output, payload) {
|
|
27
|
+
output.write(JSON.stringify(payload) + "\n");
|
|
28
|
+
}
|
|
29
|
+
function parseRouteArguments(args) {
|
|
30
|
+
const route = (0, connection_ux_1.normalizeConnectionRouteName)(typeof args?.route === "string" ? args.route : undefined);
|
|
31
|
+
const sessionId = typeof args?.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : undefined;
|
|
32
|
+
return sessionId ? { route, sessionId } : { route };
|
|
33
|
+
}
|
|
34
|
+
function routeDescriptor(route) {
|
|
35
|
+
if (route === "safari") {
|
|
36
|
+
return { browser: "safari", attachMode: "direct" };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
browser: "chrome",
|
|
40
|
+
attachMode: route === "chrome-relay" ? "relay" : "direct"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function routeTruth(route, session) {
|
|
44
|
+
const runtimeActions = session
|
|
45
|
+
? {
|
|
46
|
+
activate: session.capabilities.activate,
|
|
47
|
+
navigate: session.capabilities.navigate,
|
|
48
|
+
screenshot: session.capabilities.screenshot
|
|
49
|
+
}
|
|
50
|
+
: route.browser === "safari"
|
|
51
|
+
? { activate: true, navigate: true, screenshot: true }
|
|
52
|
+
: { activate: false, navigate: false, screenshot: false };
|
|
53
|
+
const unsupportedRuntimeActions = Object.entries(runtimeActions)
|
|
54
|
+
.filter(([, supported]) => !supported)
|
|
55
|
+
.map(([name]) => name);
|
|
56
|
+
const readOnly = session ? session.status.state === "read-only" : route.browser === "chrome";
|
|
57
|
+
const sharedTabScoped = session
|
|
58
|
+
? session.browser === "chrome" && session.attach.mode === "relay"
|
|
59
|
+
: route.browser === "chrome" && route.attachMode === "relay";
|
|
60
|
+
const actionable = session ? session.status.canAct : route.browser === "safari";
|
|
61
|
+
const notes = route.browser === "safari"
|
|
62
|
+
? ["Safari is actionable in this product surface."]
|
|
63
|
+
: sharedTabScoped
|
|
64
|
+
? [
|
|
65
|
+
"Chrome relay remains read-only in this phase.",
|
|
66
|
+
"Chrome relay only covers the currently shared tab."
|
|
67
|
+
]
|
|
68
|
+
: ["Chrome direct remains read-only in this phase."];
|
|
69
|
+
return {
|
|
70
|
+
actionable,
|
|
71
|
+
readOnly,
|
|
72
|
+
sharedTabScoped,
|
|
73
|
+
runtimeActions,
|
|
74
|
+
unsupportedRuntimeActions,
|
|
75
|
+
notes
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function toolText(payload) {
|
|
79
|
+
return JSON.stringify(payload);
|
|
80
|
+
}
|
|
81
|
+
function toolResult(payload, isError = false) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: toolText(payload) }],
|
|
84
|
+
structuredContent: payload,
|
|
85
|
+
...(isError ? { isError: true } : {})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function errorToolResult(code, message, details) {
|
|
89
|
+
return toolResult({
|
|
90
|
+
ok: false,
|
|
91
|
+
error: {
|
|
92
|
+
code,
|
|
93
|
+
message,
|
|
94
|
+
...(details === undefined ? {} : { details })
|
|
95
|
+
}
|
|
96
|
+
}, true);
|
|
97
|
+
}
|
|
98
|
+
function summarizeTabs(browser, count) {
|
|
99
|
+
const label = browser === "safari" ? "Safari" : "Chrome direct";
|
|
100
|
+
const suffix = count === 1 ? "tab" : "tabs";
|
|
101
|
+
return `Listed ${count} ${label} ${suffix}.`;
|
|
102
|
+
}
|
|
103
|
+
function browserTabsRelayBlockedResult() {
|
|
104
|
+
const route = routeDescriptor("chrome-relay");
|
|
105
|
+
const reason = {
|
|
106
|
+
code: "shared_tab_scope_only",
|
|
107
|
+
message: "Chrome relay is shared-tab scoped and not browser-wide tab enumeration."
|
|
108
|
+
};
|
|
109
|
+
return toolResult({
|
|
110
|
+
tool: "browser_tabs",
|
|
111
|
+
ok: false,
|
|
112
|
+
blocked: true,
|
|
113
|
+
outcome: "unsupported",
|
|
114
|
+
status: "unsupported",
|
|
115
|
+
category: "shared-tab-scope",
|
|
116
|
+
reason,
|
|
117
|
+
summary: "Chrome relay is shared-tab scoped and does not support browser-wide tab enumeration.",
|
|
118
|
+
prompt: "Use browser_connect for the currently shared tab, or retry browser_tabs with safari or chrome-direct.",
|
|
119
|
+
truth: routeTruth(route),
|
|
120
|
+
supportedRoutes: ["safari", "chrome-direct"],
|
|
121
|
+
blockedReason: reason
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function invalidParams(message, data) {
|
|
125
|
+
return {
|
|
126
|
+
code: -32602,
|
|
127
|
+
message,
|
|
128
|
+
...(data === undefined ? {} : { data })
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function requestIdOrNull(message) {
|
|
132
|
+
return message.id ?? null;
|
|
133
|
+
}
|
|
134
|
+
function isBrowserConnectFailure(payload) {
|
|
135
|
+
return !payload.ok || !payload.connected;
|
|
136
|
+
}
|
|
137
|
+
function firstDiagnosticBlocker(diagnostics, route) {
|
|
138
|
+
if (route.browser === "safari") {
|
|
139
|
+
return [
|
|
140
|
+
...(diagnostics.preflight?.inspect.blockers ?? []),
|
|
141
|
+
...(diagnostics.preflight?.automation.blockers ?? []),
|
|
142
|
+
...(diagnostics.preflight?.screenshot.blockers ?? [])
|
|
143
|
+
][0];
|
|
144
|
+
}
|
|
145
|
+
return (route.attachMode === "relay" ? diagnostics.attach?.relay?.blockers : diagnostics.attach?.direct?.blockers)?.[0];
|
|
146
|
+
}
|
|
147
|
+
function isUnsupportedReasonCode(code) {
|
|
148
|
+
return code === "shared_tab_scope_only"
|
|
149
|
+
|| code === "unsupported_action"
|
|
150
|
+
|| code === "unsupported_browser"
|
|
151
|
+
|| code === "activation_unavailable"
|
|
152
|
+
|| code === "navigation_unavailable"
|
|
153
|
+
|| code === "screenshot_unavailable"
|
|
154
|
+
|| code === "relay_transport_not_implemented";
|
|
155
|
+
}
|
|
156
|
+
function doctorResultFields(payload) {
|
|
157
|
+
const reason = payload.blocked ? firstDiagnosticBlocker(payload.diagnostics, payload.route) : undefined;
|
|
158
|
+
return {
|
|
159
|
+
outcome: payload.blocked ? "blocked" : "success",
|
|
160
|
+
status: payload.blocked ? "blocked" : "ready",
|
|
161
|
+
category: payload.blocked ? "route-blocked" : "route-ready",
|
|
162
|
+
...(reason ? { reason } : {})
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function connectResultFields(payload) {
|
|
166
|
+
if (payload.ok && payload.connected) {
|
|
167
|
+
return {
|
|
168
|
+
outcome: "success",
|
|
169
|
+
status: "connected",
|
|
170
|
+
category: "session-connected"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (payload.blocked) {
|
|
174
|
+
const reason = firstDiagnosticBlocker(payload.diagnostics, payload.route);
|
|
175
|
+
return {
|
|
176
|
+
outcome: "blocked",
|
|
177
|
+
status: "blocked",
|
|
178
|
+
category: "connection-blocked",
|
|
179
|
+
...(reason ? { reason } : {})
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const reason = payload.error
|
|
183
|
+
? {
|
|
184
|
+
code: payload.error.code ?? "unknown_error",
|
|
185
|
+
message: payload.error.message,
|
|
186
|
+
...(payload.errorUx?.retryable === undefined ? {} : { retryable: payload.errorUx.retryable }),
|
|
187
|
+
...(payload.errorUx?.userActionRequired === undefined ? {} : { userActionRequired: payload.errorUx.userActionRequired })
|
|
188
|
+
}
|
|
189
|
+
: undefined;
|
|
190
|
+
const unsupported = isUnsupportedReasonCode(reason?.code);
|
|
191
|
+
return {
|
|
192
|
+
outcome: unsupported ? "unsupported" : "error",
|
|
193
|
+
status: unsupported ? "unsupported" : "failed",
|
|
194
|
+
category: unsupported ? "connection-unsupported" : "connection-failed",
|
|
195
|
+
...(reason ? { reason } : {})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function tabsSuccessResultFields() {
|
|
199
|
+
return {
|
|
200
|
+
outcome: "success",
|
|
201
|
+
status: "listed",
|
|
202
|
+
category: "tab-list"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function errorResultFields(code, message, details) {
|
|
206
|
+
const unsupported = isUnsupportedReasonCode(code);
|
|
207
|
+
const reason = {
|
|
208
|
+
code,
|
|
209
|
+
message
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
outcome: unsupported ? "unsupported" : "error",
|
|
213
|
+
status: unsupported ? "unsupported" : "failed",
|
|
214
|
+
category: unsupported ? "tool-unsupported" : "tool-error",
|
|
215
|
+
reason,
|
|
216
|
+
...(details === undefined ? {} : { errorDetails: details })
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function createMcpServer(service = new attach_service_1.AttachService()) {
|
|
220
|
+
let initialized = false;
|
|
221
|
+
let protocolVersion = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
222
|
+
return {
|
|
223
|
+
async handleMessage(message) {
|
|
224
|
+
if (message.method === "notifications/initialized") {
|
|
225
|
+
initialized = true;
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (!isRequest(message)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
if (message.method === "initialize") {
|
|
232
|
+
protocolVersion = negotiateProtocolVersion(message.params?.protocolVersion);
|
|
233
|
+
return {
|
|
234
|
+
jsonrpc: "2.0",
|
|
235
|
+
id: requestIdOrNull(message),
|
|
236
|
+
result: {
|
|
237
|
+
protocolVersion,
|
|
238
|
+
capabilities: {
|
|
239
|
+
tools: {}
|
|
240
|
+
},
|
|
241
|
+
serverInfo: {
|
|
242
|
+
name: SERVER_NAME,
|
|
243
|
+
version: "0.1.0"
|
|
244
|
+
},
|
|
245
|
+
instructions: "Use browser_doctor before browser_connect when route readiness is unclear. browser_tabs is available for Safari and Chrome direct. Chrome relay is read-only, limited to the currently shared tab, and does not support browser-wide tab enumeration."
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (message.method === "ping") {
|
|
250
|
+
return {
|
|
251
|
+
jsonrpc: "2.0",
|
|
252
|
+
id: requestIdOrNull(message),
|
|
253
|
+
result: {}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (!initialized) {
|
|
257
|
+
return {
|
|
258
|
+
jsonrpc: "2.0",
|
|
259
|
+
id: requestIdOrNull(message),
|
|
260
|
+
error: {
|
|
261
|
+
code: -32002,
|
|
262
|
+
message: "Server not initialized."
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (message.method === "tools/list") {
|
|
267
|
+
return {
|
|
268
|
+
jsonrpc: "2.0",
|
|
269
|
+
id: requestIdOrNull(message),
|
|
270
|
+
result: {
|
|
271
|
+
tools: [
|
|
272
|
+
{
|
|
273
|
+
name: "browser_doctor",
|
|
274
|
+
title: "Browser Doctor",
|
|
275
|
+
description: "Check whether the requested route is ready before connect. Safari can be actionable. Chrome direct stays read-only. Chrome relay stays read-only and only covers the currently shared tab.",
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: "object",
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
properties: {
|
|
280
|
+
route: {
|
|
281
|
+
type: "string",
|
|
282
|
+
enum: ["safari", "chrome-direct", "chrome-relay"]
|
|
283
|
+
},
|
|
284
|
+
sessionId: {
|
|
285
|
+
type: "string",
|
|
286
|
+
description: "Optional saved session ID to check resume readiness instead of a fresh attach."
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
required: ["route"]
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "browser_tabs",
|
|
294
|
+
title: "Browser Tabs",
|
|
295
|
+
description: "List tabs only for browser-wide safe contexts. Safari and Chrome direct can enumerate tabs. Chrome relay returns a structured blocked result because relay is shared-tab scoped, not browser-wide.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
additionalProperties: false,
|
|
299
|
+
properties: {
|
|
300
|
+
route: {
|
|
301
|
+
type: "string",
|
|
302
|
+
enum: ["safari", "chrome-direct", "chrome-relay"]
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
required: ["route"]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "browser_connect",
|
|
310
|
+
title: "Browser Connect",
|
|
311
|
+
description: "Attach or resume the requested route-first session. Returns explicit read-only, shared-tab scope, and unsupported runtime action metadata.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
properties: {
|
|
316
|
+
route: {
|
|
317
|
+
type: "string",
|
|
318
|
+
enum: ["safari", "chrome-direct", "chrome-relay"]
|
|
319
|
+
},
|
|
320
|
+
sessionId: {
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "Optional saved session ID to resume."
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
required: ["route"]
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (message.method !== "tools/call") {
|
|
333
|
+
return {
|
|
334
|
+
jsonrpc: "2.0",
|
|
335
|
+
id: requestIdOrNull(message),
|
|
336
|
+
error: {
|
|
337
|
+
code: -32601,
|
|
338
|
+
message: `Method not found: ${message.method}`
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const toolName = typeof message.params?.name === "string" ? message.params.name : undefined;
|
|
343
|
+
const args = typeof message.params?.arguments === "object" && message.params.arguments !== null
|
|
344
|
+
? message.params.arguments
|
|
345
|
+
: undefined;
|
|
346
|
+
if (!toolName) {
|
|
347
|
+
return {
|
|
348
|
+
jsonrpc: "2.0",
|
|
349
|
+
id: requestIdOrNull(message),
|
|
350
|
+
error: invalidParams("tools/call requires a string params.name.")
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
if (toolName === "browser_doctor") {
|
|
355
|
+
const route = parseRouteArguments(args);
|
|
356
|
+
const payload = await (0, connection_ux_1.doctorConnectionRoute)(service, route);
|
|
357
|
+
return {
|
|
358
|
+
jsonrpc: "2.0",
|
|
359
|
+
id: requestIdOrNull(message),
|
|
360
|
+
result: toolResult({
|
|
361
|
+
tool: "browser_doctor",
|
|
362
|
+
ok: payload.ok,
|
|
363
|
+
blocked: payload.blocked,
|
|
364
|
+
...doctorResultFields(payload),
|
|
365
|
+
summary: payload.summary,
|
|
366
|
+
prompt: payload.prompt,
|
|
367
|
+
nextStep: payload.nextStep,
|
|
368
|
+
truth: routeTruth(payload.route),
|
|
369
|
+
envelope: payload
|
|
370
|
+
})
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (toolName === "browser_connect") {
|
|
374
|
+
const route = parseRouteArguments(args);
|
|
375
|
+
const payload = await (0, connection_ux_1.connectConnectionRoute)(service, route);
|
|
376
|
+
return {
|
|
377
|
+
jsonrpc: "2.0",
|
|
378
|
+
id: requestIdOrNull(message),
|
|
379
|
+
result: toolResult({
|
|
380
|
+
tool: "browser_connect",
|
|
381
|
+
ok: payload.ok,
|
|
382
|
+
connected: payload.connected,
|
|
383
|
+
...connectResultFields(payload),
|
|
384
|
+
summary: payload.summary,
|
|
385
|
+
prompt: payload.prompt,
|
|
386
|
+
nextStep: payload.nextStep,
|
|
387
|
+
truth: routeTruth(payload.route, payload.session),
|
|
388
|
+
envelope: payload
|
|
389
|
+
}, isBrowserConnectFailure(payload))
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (toolName === "browser_tabs") {
|
|
393
|
+
const routeName = parseRouteArguments(args).route;
|
|
394
|
+
if (routeName === "chrome-relay") {
|
|
395
|
+
return {
|
|
396
|
+
jsonrpc: "2.0",
|
|
397
|
+
id: requestIdOrNull(message),
|
|
398
|
+
result: browserTabsRelayBlockedResult()
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const route = routeDescriptor(routeName);
|
|
402
|
+
const tabs = await service.listTabs(route.browser);
|
|
403
|
+
return {
|
|
404
|
+
jsonrpc: "2.0",
|
|
405
|
+
id: requestIdOrNull(message),
|
|
406
|
+
result: toolResult({
|
|
407
|
+
tool: "browser_tabs",
|
|
408
|
+
ok: true,
|
|
409
|
+
blocked: false,
|
|
410
|
+
...tabsSuccessResultFields(),
|
|
411
|
+
summary: summarizeTabs(route.browser, tabs.length),
|
|
412
|
+
truth: routeTruth(route),
|
|
413
|
+
count: tabs.length,
|
|
414
|
+
tabs
|
|
415
|
+
})
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
jsonrpc: "2.0",
|
|
420
|
+
id: requestIdOrNull(message),
|
|
421
|
+
error: invalidParams(`Unsupported tool: ${toolName}`)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const { payload } = (0, errors_1.toErrorPayload)(error);
|
|
426
|
+
return {
|
|
427
|
+
jsonrpc: "2.0",
|
|
428
|
+
id: requestIdOrNull(message),
|
|
429
|
+
result: toolResult({
|
|
430
|
+
ok: false,
|
|
431
|
+
error: {
|
|
432
|
+
code: payload.error.code,
|
|
433
|
+
message: payload.error.message,
|
|
434
|
+
...(payload.error.details === undefined ? {} : { details: payload.error.details })
|
|
435
|
+
},
|
|
436
|
+
...errorResultFields(payload.error.code, payload.error.message, payload.error.details)
|
|
437
|
+
}, true)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function runMcpStdioServer(options = {}) {
|
|
444
|
+
const server = createMcpServer(options.service);
|
|
445
|
+
const input = options.input ?? process.stdin;
|
|
446
|
+
const output = options.output ?? process.stdout;
|
|
447
|
+
const error = options.error ?? process.stderr;
|
|
448
|
+
const reader = (0, node_readline_1.createInterface)({ input, crlfDelay: Infinity });
|
|
449
|
+
let chain = Promise.resolve();
|
|
450
|
+
reader.on("line", (line) => {
|
|
451
|
+
if (!line.trim()) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
chain = chain.then(async () => {
|
|
455
|
+
try {
|
|
456
|
+
const parsed = JSON.parse(line);
|
|
457
|
+
if (!isJsonRpcMessage(parsed)) {
|
|
458
|
+
throw new errors_1.AppError("Message must be a JSON-RPC 2.0 request or notification.", 400, "invalid_request");
|
|
459
|
+
}
|
|
460
|
+
const response = await server.handleMessage(parsed);
|
|
461
|
+
if (response !== undefined) {
|
|
462
|
+
writeMessage(output, response);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (errorValue) {
|
|
466
|
+
writeMessage(output, {
|
|
467
|
+
jsonrpc: "2.0",
|
|
468
|
+
id: null,
|
|
469
|
+
error: errorValue instanceof SyntaxError
|
|
470
|
+
? {
|
|
471
|
+
code: -32700,
|
|
472
|
+
message: "Parse error"
|
|
473
|
+
}
|
|
474
|
+
: errorValue instanceof errors_1.AppError
|
|
475
|
+
? invalidParams(errorValue.message, { code: errorValue.code })
|
|
476
|
+
: invalidParams("Invalid JSON-RPC message.")
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}).catch((errorValue) => {
|
|
480
|
+
error.write(`${errorValue instanceof Error ? errorValue.stack ?? errorValue.message : String(errorValue)}\n`);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type BrowserAttachUxInterpretation } from "./browser-attach-ux-helper";
|
|
2
|
+
import type { AttachmentSession, BrowserAttachMode, BrowserDiagnostics, BridgeCapabilitiesContract, ChromeRelayFailureOperation, ResumedSession, SupportedBrowser } from "./types";
|
|
3
|
+
export interface BridgeAttachRoute {
|
|
4
|
+
browser: SupportedBrowser;
|
|
5
|
+
attachMode?: BrowserAttachMode;
|
|
6
|
+
}
|
|
7
|
+
export interface BridgeResumeRoute extends BridgeAttachRoute {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
}
|
|
10
|
+
export type BridgeRoute = BridgeAttachRoute | BridgeResumeRoute;
|
|
11
|
+
export type BridgeSessionResult = AttachmentSession | ResumedSession | {
|
|
12
|
+
session: AttachmentSession;
|
|
13
|
+
};
|
|
14
|
+
export interface BridgeAdapter<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends BridgeSessionResult = BridgeSessionResult> {
|
|
15
|
+
getCapabilities(): Promise<TCapabilities>;
|
|
16
|
+
getDiagnostics(browser: SupportedBrowser): Promise<BrowserDiagnostics>;
|
|
17
|
+
attach(args: BridgeAttachRoute): Promise<TAttachResult>;
|
|
18
|
+
resume(sessionId: string): Promise<TResumeResult>;
|
|
19
|
+
}
|
|
20
|
+
export interface BridgeConnectionResult<TCapabilities, TResult extends BridgeSessionResult> {
|
|
21
|
+
capabilities: TCapabilities;
|
|
22
|
+
diagnostics: BrowserDiagnostics;
|
|
23
|
+
operation: ChromeRelayFailureOperation;
|
|
24
|
+
route: BridgeRoute;
|
|
25
|
+
routeUx: BrowserAttachUxInterpretation;
|
|
26
|
+
result: TResult;
|
|
27
|
+
session: AttachmentSession;
|
|
28
|
+
sessionUx: BrowserAttachUxInterpretation;
|
|
29
|
+
}
|
|
30
|
+
export declare function createBridgeAdapter<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends BridgeSessionResult = BridgeSessionResult>(adapter: BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>): BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>;
|
|
31
|
+
export declare function sessionFromBridgeResult<TResult extends BridgeSessionResult>(result: TResult): AttachmentSession;
|
|
32
|
+
export declare function connectViaBridge<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends BridgeSessionResult = BridgeSessionResult>(adapter: BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>, route: BridgeRoute): Promise<BridgeConnectionResult<TCapabilities, TAttachResult | TResumeResult>>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBridgeAdapter = createBridgeAdapter;
|
|
4
|
+
exports.sessionFromBridgeResult = sessionFromBridgeResult;
|
|
5
|
+
exports.connectViaBridge = connectViaBridge;
|
|
6
|
+
const browser_attach_ux_helper_1 = require("./browser-attach-ux-helper");
|
|
7
|
+
function hasSession(result) {
|
|
8
|
+
return typeof result === "object" && result !== null && "session" in result;
|
|
9
|
+
}
|
|
10
|
+
function createBridgeAdapter(adapter) {
|
|
11
|
+
return adapter;
|
|
12
|
+
}
|
|
13
|
+
function sessionFromBridgeResult(result) {
|
|
14
|
+
return hasSession(result) ? result.session : result;
|
|
15
|
+
}
|
|
16
|
+
async function connectViaBridge(adapter, route) {
|
|
17
|
+
const capabilities = await adapter.getCapabilities();
|
|
18
|
+
const diagnostics = await adapter.getDiagnostics(route.browser);
|
|
19
|
+
const operation = "sessionId" in route ? "resumeSession" : "attach";
|
|
20
|
+
const attachMode = route.attachMode ?? (route.browser === "chrome" ? "direct" : "direct");
|
|
21
|
+
const routeUx = (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromDiagnostics)({
|
|
22
|
+
browser: route.browser,
|
|
23
|
+
attachMode,
|
|
24
|
+
diagnostics,
|
|
25
|
+
operation
|
|
26
|
+
});
|
|
27
|
+
const result = "sessionId" in route ? await adapter.resume(route.sessionId) : await adapter.attach({ browser: route.browser, attachMode });
|
|
28
|
+
const session = sessionFromBridgeResult(result);
|
|
29
|
+
return {
|
|
30
|
+
capabilities,
|
|
31
|
+
diagnostics,
|
|
32
|
+
operation,
|
|
33
|
+
route,
|
|
34
|
+
routeUx,
|
|
35
|
+
result,
|
|
36
|
+
session,
|
|
37
|
+
sessionUx: (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromSession)({
|
|
38
|
+
session,
|
|
39
|
+
operation
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|