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 +21 -0
- package/README.md +114 -0
- package/package.json +30 -0
- package/src/auth.js +139 -0
- package/src/index.js +235 -0
- package/src/parser.js +37 -0
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
|
+
}
|