mcp-web-bridge 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hemanth HM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # mcp-web-bridge
2
+
3
+ Connect any MCP server to Chrome's WebMCP API.
4
+
5
+ You have MCP servers. You want them in the browser. This module connects to a remote MCP server, discovers its tools, and registers them with `navigator.modelContext`.
6
+
7
+ ```bash
8
+ npm install mcp-web-bridge
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import { WebMCPBridge } from 'mcp-web-bridge';
15
+
16
+ const bridge = new WebMCPBridge('https://mcp.example.com');
17
+ await bridge.connect();
18
+ bridge.register();
19
+ ```
20
+
21
+ That's it. Tools are now available to Chrome's AI agent.
22
+
23
+ ## With context enrichment
24
+
25
+ ```javascript
26
+ const bridge = new WebMCPBridge('https://mcp.example.com', {
27
+ enrichContext: (toolName, args) => ({
28
+ ...args,
29
+ user_locale: navigator.language,
30
+ user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
31
+ }),
32
+ onResponse: (toolName, result) => {
33
+ console.log(`[${toolName}]`, result);
34
+ return result;
35
+ },
36
+ });
37
+
38
+ await bridge.connect();
39
+ bridge.register();
40
+ ```
41
+
42
+ ## With auth
43
+
44
+ ```javascript
45
+ const bridge = new WebMCPBridge('https://mcp.example.com');
46
+ bridge.setAuth({ type: 'bearer', token: 'sk-...' });
47
+ await bridge.connect();
48
+ ```
49
+
50
+ Supports `bearer`, `apikey`, and `basic` auth. OAuth with PKCE is handled by the `MCPAuth` class.
51
+
52
+ ## Custom headers
53
+
54
+ ```javascript
55
+ const bridge = new WebMCPBridge('https://mcp.example.com', {
56
+ headers: {
57
+ 'X-Custom-Header': 'value',
58
+ 'Authorization': 'Bearer sk-...',
59
+ },
60
+ });
61
+ ```
62
+
63
+ Custom headers are merged into every request. Auth headers from `setAuth()` are applied first, then your custom headers override.
64
+
65
+ ## Add page-local tools
66
+
67
+ ```javascript
68
+ bridge.register([
69
+ {
70
+ name: 'get_selection',
71
+ description: 'Get the currently selected text on the page',
72
+ inputSchema: { type: 'object', properties: {} },
73
+ execute: async () => ({
74
+ content: [{ type: 'text', text: window.getSelection().toString() }],
75
+ }),
76
+ },
77
+ ]);
78
+ ```
79
+
80
+ Mix remote MCP tools with local browser capabilities.
81
+
82
+ ## API
83
+
84
+ ### `new WebMCPBridge(serverUrl, options?)`
85
+
86
+ | Option | Type | Description |
87
+ |--------|------|-------------|
88
+ | `headers` | `object` | Custom headers merged into every request |
89
+ | `enrichContext` | `(name, args) => args` | Enrich tool args before proxying |
90
+ | `onToolCall` | `(name, args) => void` | Called before each tool call |
91
+ | `onResponse` | `(name, result) => result` | Transform responses |
92
+ | `onError` | `(name, error) => void` | Error handler |
93
+ | `logger` | `object` | Custom logger (default: `console`) |
94
+
95
+ ### Methods
96
+
97
+ | Method | Returns | Description |
98
+ |--------|---------|-------------|
99
+ | `connect()` | `{ tools, prompts, resources }` | Initialize + discover |
100
+ | `register(extraTools?)` | `tool[]` | Register with WebMCP |
101
+ | `callTool(name, args)` | `result` | Call a tool |
102
+ | `getPrompt(name, args)` | `result` | Get a prompt |
103
+ | `readResource(uri)` | `result` | Read a resource |
104
+ | `setAuth({ type, token })` | — | Set auth before connecting |
105
+ | `disconnect()` | — | Clear context + logout |
106
+
107
+ ## Browser requirements
108
+
109
+ - Chrome 146+ with `chrome://flags/#enable-webmcp-testing`
110
+ - Without WebMCP, `connect()` and `callTool()` still work — you just can't `register()`
111
+
112
+ ## License
113
+
114
+ MIT
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "mcp-web-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Connect any MCP server to Chrome's WebMCP API",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "files": [
12
+ "src/"
13
+ ],
14
+ "keywords": [
15
+ "mcp",
16
+ "webmcp",
17
+ "model-context-protocol",
18
+ "chrome",
19
+ "ai",
20
+ "agents",
21
+ "browser"
22
+ ],
23
+ "author": "Hemanth HM <hemanth.hm@gmail.com>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/hemanth/webmcp-bridge"
28
+ },
29
+ "homepage": "https://github.com/hemanth/webmcp-bridge"
30
+ }
package/src/auth.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * MCP Auth — handles OAuth (PKCE), API key, Basic, and Bearer auth.
3
+ */
4
+ export class MCPAuth {
5
+ constructor(serverUrl) {
6
+ this.serverUrl = serverUrl;
7
+ this.token = null;
8
+ this.tokenType = 'Bearer';
9
+ this.clientId = null;
10
+ this.authMeta = null;
11
+ this.sessionId = null;
12
+ }
13
+
14
+ async discover() {
15
+ const baseUrl = new URL(this.serverUrl).origin;
16
+ const metaUrl = `${baseUrl}/.well-known/oauth-authorization-server`;
17
+
18
+ try {
19
+ const res = await fetch(metaUrl, {
20
+ method: 'GET',
21
+ headers: { Accept: 'application/json' },
22
+ });
23
+
24
+ if (res.status === 404 || !res.ok) {
25
+ return { type: 'none', requiresAuth: false };
26
+ }
27
+
28
+ const meta = await res.json();
29
+ this.authMeta = meta;
30
+
31
+ const thirdParty = meta.identity_providers || meta.supported_identity_providers || [];
32
+
33
+ return {
34
+ type: 'oauth',
35
+ requiresAuth: true,
36
+ meta,
37
+ isOwnIdp: thirdParty.length === 0 && !!meta.authorization_endpoint,
38
+ thirdParty,
39
+ supportsRegistration: !!meta.registration_endpoint,
40
+ };
41
+ } catch {
42
+ return { type: 'none', requiresAuth: false };
43
+ }
44
+ }
45
+
46
+ async registerClient(redirectUri) {
47
+ if (!this.authMeta?.registration_endpoint) {
48
+ throw new Error('Server does not support dynamic client registration');
49
+ }
50
+
51
+ const res = await fetch(this.authMeta.registration_endpoint, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({
55
+ client_name: 'WebMCP Bridge',
56
+ redirect_uris: [redirectUri],
57
+ grant_types: ['authorization_code', 'refresh_token'],
58
+ response_types: ['code'],
59
+ token_endpoint_auth_method: 'none',
60
+ scope: 'openid profile mcp:tools mcp:read mcp:write',
61
+ }),
62
+ });
63
+
64
+ if (!res.ok) {
65
+ const err = await res.json().catch(() => ({}));
66
+ throw new Error(err.error_description || 'Client registration failed');
67
+ }
68
+
69
+ const data = await res.json();
70
+ this.clientId = data.client_id;
71
+ return data;
72
+ }
73
+
74
+ async generatePKCE() {
75
+ const array = new Uint8Array(32);
76
+ crypto.getRandomValues(array);
77
+ const verifier = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
78
+
79
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
80
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
81
+ .replace(/\+/g, '-')
82
+ .replace(/\//g, '_')
83
+ .replace(/=+$/, '');
84
+
85
+ return { verifier, challenge };
86
+ }
87
+
88
+ setApiKey(apiKey) {
89
+ this.token = apiKey;
90
+ this.tokenType = 'X-API-Key';
91
+ }
92
+
93
+ setBasicAuth(username, password) {
94
+ this.token = btoa(`${username}:${password}`);
95
+ this.tokenType = 'Basic';
96
+ }
97
+
98
+ setBearerToken(token) {
99
+ this.token = token;
100
+ this.tokenType = 'Bearer';
101
+ }
102
+
103
+ setSessionId(id) {
104
+ this.sessionId = id;
105
+ }
106
+
107
+ getHeaders(includeSession = true) {
108
+ const headers = {
109
+ 'Content-Type': 'application/json',
110
+ Accept: 'application/json, text/event-stream',
111
+ };
112
+
113
+ if (this.token) {
114
+ if (this.tokenType === 'X-API-Key') {
115
+ headers['X-API-Key'] = this.token;
116
+ } else if (this.tokenType === 'Basic') {
117
+ headers['Authorization'] = `Basic ${this.token}`;
118
+ } else {
119
+ headers['Authorization'] = `Bearer ${this.token}`;
120
+ }
121
+ }
122
+
123
+ if (includeSession && this.sessionId) {
124
+ headers['Mcp-Session-Id'] = this.sessionId;
125
+ }
126
+
127
+ return headers;
128
+ }
129
+
130
+ isAuthenticated() {
131
+ return !!this.token;
132
+ }
133
+
134
+ logout() {
135
+ this.token = null;
136
+ this.clientId = null;
137
+ this.sessionId = null;
138
+ }
139
+ }
package/src/index.js ADDED
@@ -0,0 +1,235 @@
1
+ import { MCPAuth } from './auth.js';
2
+ import { parseSSEorJSON } from './parser.js';
3
+
4
+ export { MCPAuth } from './auth.js';
5
+ export { parseSSEorJSON } from './parser.js';
6
+
7
+ /**
8
+ * WebMCP Bridge — connect any MCP server to Chrome's WebMCP API.
9
+ *
10
+ * @example
11
+ * const bridge = new WebMCPBridge('https://mcp.example.com');
12
+ * await bridge.connect();
13
+ * // Tools are now registered with navigator.modelContext
14
+ */
15
+ export class WebMCPBridge {
16
+ constructor(serverUrl, options = {}) {
17
+ this.serverUrl = serverUrl;
18
+ this.auth = new MCPAuth(serverUrl);
19
+ this.tools = [];
20
+ this.prompts = [];
21
+ this.resources = [];
22
+ this.serverInfo = null;
23
+ this.capabilities = null;
24
+
25
+ // Custom headers merged into every request
26
+ this.headers = options.headers || {};
27
+
28
+ // Hooks
29
+ this.onToolCall = options.onToolCall || null;
30
+ this.onResponse = options.onResponse || null;
31
+ this.onError = options.onError || null;
32
+ this.enrichContext = options.enrichContext || null;
33
+ this.logger = options.logger || console;
34
+ }
35
+
36
+ /**
37
+ * Set auth credentials before connecting.
38
+ */
39
+ setAuth({ type, token, username, password }) {
40
+ if (type === 'bearer') this.auth.setBearerToken(token);
41
+ else if (type === 'apikey') this.auth.setApiKey(token);
42
+ else if (type === 'basic') this.auth.setBasicAuth(username, password);
43
+ }
44
+
45
+ /**
46
+ * Connect to the MCP server, discover capabilities, register with WebMCP.
47
+ */
48
+ async connect() {
49
+ // 1. Initialize session
50
+ const initPayload = {
51
+ jsonrpc: '2.0',
52
+ id: 1,
53
+ method: 'initialize',
54
+ params: {
55
+ protocolVersion: '2024-11-05',
56
+ capabilities: { tools: {} },
57
+ clientInfo: { name: 'webmcp-bridge', version: '1.0.0' },
58
+ },
59
+ };
60
+
61
+ const initResponse = await fetch(this.serverUrl, {
62
+ method: 'POST',
63
+ headers: this._getHeaders(false),
64
+ body: JSON.stringify(initPayload),
65
+ });
66
+
67
+ if (initResponse.status === 401 || initResponse.status === 403) {
68
+ throw new Error('Authentication required');
69
+ }
70
+
71
+ const initData = await parseSSEorJSON(initResponse);
72
+ if (initData.error) throw new Error(initData.error.message || 'Initialize failed');
73
+
74
+ // Capture session ID
75
+ const sessionId = initResponse.headers.get('Mcp-Session-Id');
76
+ if (sessionId) this.auth.setSessionId(sessionId);
77
+
78
+ this.serverInfo = initData.result?.serverInfo || null;
79
+ this.capabilities = initData.result?.capabilities || null;
80
+
81
+ // 2. Discover tools, prompts, resources
82
+ this.tools = await this._list('tools/list', 'tools');
83
+ this.prompts = await this._list('prompts/list', 'prompts');
84
+ this.resources = await this._list('resources/list', 'resources');
85
+
86
+ this.logger.log(`[webmcp-bridge] Connected. ${this.tools.length} tools, ${this.prompts.length} prompts, ${this.resources.length} resources.`);
87
+
88
+ return {
89
+ serverInfo: this.serverInfo,
90
+ capabilities: this.capabilities,
91
+ tools: this.tools,
92
+ prompts: this.prompts,
93
+ resources: this.resources,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Register discovered tools with navigator.modelContext (WebMCP).
99
+ * Call after connect(). Optionally pass extra page-local tools.
100
+ */
101
+ register(extraTools = []) {
102
+ if (!navigator.modelContext) {
103
+ throw new Error('navigator.modelContext not available. Enable chrome://flags/#enable-webmcp-testing');
104
+ }
105
+
106
+ const webMCPTools = this.tools.map(tool => ({
107
+ name: tool.name,
108
+ description: tool.description || '',
109
+ inputSchema: tool.inputSchema || { type: 'object', properties: {} },
110
+ execute: async (args) => this.callTool(tool.name, args),
111
+ }));
112
+
113
+ const allTools = [...webMCPTools, ...extraTools];
114
+
115
+ navigator.modelContext.provideContext({ tools: allTools });
116
+ this.logger.log(`[webmcp-bridge] Registered ${allTools.length} tools with WebMCP.`);
117
+
118
+ return allTools;
119
+ }
120
+
121
+ /**
122
+ * Call a tool on the remote MCP server.
123
+ */
124
+ async callTool(name, args = {}) {
125
+ // Enrich args if hook is provided
126
+ const enriched = this.enrichContext ? await this.enrichContext(name, args) : args;
127
+
128
+ // Notify onToolCall hook
129
+ if (this.onToolCall) this.onToolCall(name, enriched);
130
+
131
+ const payload = {
132
+ jsonrpc: '2.0',
133
+ id: Date.now(),
134
+ method: 'tools/call',
135
+ params: { name, arguments: enriched },
136
+ };
137
+
138
+ try {
139
+ const response = await fetch(this.serverUrl, {
140
+ method: 'POST',
141
+ headers: this._getHeaders(),
142
+ body: JSON.stringify(payload),
143
+ });
144
+
145
+ const result = await parseSSEorJSON(response);
146
+
147
+ if (result.error) {
148
+ const err = new Error(result.error.message || 'Tool call failed');
149
+ if (this.onError) this.onError(name, err);
150
+ return { content: [{ type: 'text', text: `Error: ${result.error.message}` }] };
151
+ }
152
+
153
+ // Transform response if hook is provided
154
+ let output = result.result?.content ? result.result : {
155
+ content: [{
156
+ type: 'text',
157
+ text: typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2),
158
+ }],
159
+ };
160
+
161
+ if (this.onResponse) output = await this.onResponse(name, output) || output;
162
+
163
+ return output;
164
+ } catch (error) {
165
+ if (this.onError) this.onError(name, error);
166
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get a prompt from the server.
172
+ */
173
+ async getPrompt(name, args = {}) {
174
+ return this._rpc('prompts/get', { name, arguments: args });
175
+ }
176
+
177
+ /**
178
+ * Read a resource from the server.
179
+ */
180
+ async readResource(uri) {
181
+ return this._rpc('resources/read', { uri });
182
+ }
183
+
184
+ /**
185
+ * Disconnect — clear context.
186
+ */
187
+ disconnect() {
188
+ if (navigator.modelContext?.clearContext) {
189
+ navigator.modelContext.clearContext();
190
+ }
191
+ this.tools = [];
192
+ this.prompts = [];
193
+ this.resources = [];
194
+ this.auth.logout();
195
+ this.logger.log('[webmcp-bridge] Disconnected.');
196
+ }
197
+
198
+ // --- Internal ---
199
+
200
+ _getHeaders(includeSession = true) {
201
+ return { ...this.auth.getHeaders(includeSession), ...this.headers };
202
+ }
203
+
204
+ async _list(method, key) {
205
+ try {
206
+ const result = await this._rpc(method, {});
207
+ return result?.[key] || [];
208
+ } catch {
209
+ return [];
210
+ }
211
+ }
212
+
213
+ async _rpc(method, params) {
214
+ const response = await fetch(this.serverUrl, {
215
+ method: 'POST',
216
+ headers: this._getHeaders(),
217
+ body: JSON.stringify({
218
+ jsonrpc: '2.0',
219
+ id: Date.now(),
220
+ method,
221
+ params,
222
+ }),
223
+ });
224
+
225
+ if (response.status === 401 || response.status === 403) {
226
+ throw new Error('Authentication failed');
227
+ }
228
+
229
+ const data = await parseSSEorJSON(response);
230
+ if (data.error) throw new Error(data.error.message || `${method} failed`);
231
+ return data.result;
232
+ }
233
+ }
234
+
235
+ export default WebMCPBridge;
package/src/parser.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Parse SSE or JSON response from MCP server.
3
+ */
4
+ export async function parseSSEorJSON(response) {
5
+ const contentType = response.headers.get('content-type') || '';
6
+ const text = await response.text();
7
+
8
+ if (contentType.includes('application/json')) {
9
+ return JSON.parse(text);
10
+ }
11
+
12
+ if (contentType.includes('text/event-stream') || text.startsWith('event:')) {
13
+ const events = text.split('\n\n').map(c => c.trim()).filter(Boolean);
14
+ const payloads = [];
15
+
16
+ for (const chunk of events) {
17
+ const data = chunk
18
+ .split('\n')
19
+ .filter(l => l.startsWith('data:'))
20
+ .map(l => l.substring(5).trim())
21
+ .filter(Boolean)
22
+ .join('\n');
23
+ if (!data) continue;
24
+ try { payloads.push(JSON.parse(data)); } catch { }
25
+ }
26
+
27
+ if (payloads.length > 0) return payloads[payloads.length - 1];
28
+
29
+ try { return JSON.parse(text); } catch {
30
+ throw new Error(`Failed to parse SSE response: ${text.substring(0, 100)}...`);
31
+ }
32
+ }
33
+
34
+ try { return JSON.parse(text); } catch {
35
+ throw new Error(`Unexpected response format: ${text.substring(0, 100)}...`);
36
+ }
37
+ }