@ractive-ch/tab-mcp 0.1.0-alpha.0 → 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 +131 -0
- package/index.d.ts +1 -1
- package/index.js +13 -13
- package/index.js.map +6 -6
- package/main.js +13 -13
- package/main.js.map +6 -6
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @ractive-ch/tab-mcp
|
|
2
|
+
|
|
3
|
+
The tab-mcp relay. It lets AI apps (Claude, ChatGPT, Claude Code) call
|
|
4
|
+
[MCP](https://modelcontextprotocol.io) tools that a web page offers while it is open in a
|
|
5
|
+
browser tab.
|
|
6
|
+
|
|
7
|
+
The page connects to the relay over a WebSocket with the browser library
|
|
8
|
+
[`@ractive-ch/tab-mcp-client`](https://www.npmjs.com/package/@ractive-ch/tab-mcp-client)
|
|
9
|
+
and gets an MCP address. The user adds that address to an AI app and pairs it by typing
|
|
10
|
+
a code from the page on the relay's sign-in page. The relay then forwards the AI app's
|
|
11
|
+
requests to the page. It knows nothing about the tools themselves.
|
|
12
|
+
|
|
13
|
+
**Status: 0.1.0.** Until 1.0, a new minor version can still change settings and the API.
|
|
14
|
+
The relay speaks MCP revision 2026-07-28 only and accepts AI apps that register through
|
|
15
|
+
OAuth Client ID Metadata Documents; see [compatibility](https://github.com/ractive/tab-mcp/blob/main/docs/compatibility.md)
|
|
16
|
+
for which apps work.
|
|
17
|
+
|
|
18
|
+
## Run it
|
|
19
|
+
|
|
20
|
+
The relay needs [Bun](https://bun.sh) 1.4.3 or newer. It does not run on Node.js.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bun add --global @ractive-ch/tab-mcp
|
|
24
|
+
TAB_MCP_PUBLIC_URL=https://relay.example.com \
|
|
25
|
+
TAB_MCP_ALLOWED_ORIGINS=https://app.example.com \
|
|
26
|
+
tab-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or with Docker (linux/amd64 and linux/arm64):
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
docker run -d -p 127.0.0.1:8080:8080 -v tab-mcp-data:/data \
|
|
33
|
+
-e TAB_MCP_PUBLIC_URL=https://relay.example.com \
|
|
34
|
+
-e TAB_MCP_ALLOWED_ORIGINS=https://app.example.com \
|
|
35
|
+
ractive/tab-mcp:0.1.0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The image is also on `ghcr.io/ractive/tab-mcp`. Put the relay behind a reverse proxy
|
|
39
|
+
with TLS; see
|
|
40
|
+
[self-hosting](https://github.com/ractive/tab-mcp/blob/main/docs/self-hosting.md) for
|
|
41
|
+
WebSockets, event streams and proxy time limits.
|
|
42
|
+
|
|
43
|
+
For a local test with a page on `http://localhost:3000`:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
NODE_ENV=development TAB_MCP_PUBLIC_URL=http://localhost:8080 \
|
|
47
|
+
TAB_MCP_ALLOWED_ORIGINS=http://localhost:3000 TAB_MCP_STORE=memory tab-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Environment variables, all listed by `tab-mcp --help` and in
|
|
53
|
+
[configuration](https://github.com/ractive/tab-mcp/blob/main/docs/configuration.md).
|
|
54
|
+
The most important:
|
|
55
|
+
|
|
56
|
+
| Variable | Default | Meaning |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `TAB_MCP_PUBLIC_URL` | required | the relay's public origin; `https:` outside development |
|
|
59
|
+
| `TAB_MCP_ALLOWED_ORIGINS` | empty: no page may connect | comma-separated page origins; `*` makes an open relay (read the warning in the docs first) |
|
|
60
|
+
| `TAB_MCP_SITES` | empty | JSON array of sites with their own app name or limits |
|
|
61
|
+
| `TAB_MCP_STORE` | `sqlite` | `sqlite` or `memory` |
|
|
62
|
+
| `TAB_MCP_SQLITE_PATH` | `tab-mcp.db` | the SQLite file for grants and token hashes |
|
|
63
|
+
| `TAB_MCP_TRUST_PROXY` | `none` | `x-real-ip` or `x-forwarded-for` behind a proxy |
|
|
64
|
+
| `TAB_MCP_PORT` | `8080` | listen port |
|
|
65
|
+
|
|
66
|
+
`GET /healthz` answers `200` with the version, for health checks.
|
|
67
|
+
|
|
68
|
+
## Use it as a library
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createRelay } from "@ractive-ch/tab-mcp";
|
|
72
|
+
|
|
73
|
+
const relay = createRelay({
|
|
74
|
+
publicUrl: "https://relay.example.com",
|
|
75
|
+
allowedOrigins: ["https://app.example.com"],
|
|
76
|
+
store: "sqlite",
|
|
77
|
+
sqlitePath: "/var/lib/tab-mcp/tab-mcp.db",
|
|
78
|
+
});
|
|
79
|
+
console.log(`listening on port ${relay.port}`);
|
|
80
|
+
|
|
81
|
+
process.on("SIGTERM", () => relay.stop().then(() => process.exit(0)));
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`createRelay(options)` starts the server and returns once it listens. The options mean
|
|
85
|
+
the same as the environment variables, with camel-case names, milliseconds instead of
|
|
86
|
+
seconds and arrays instead of comma-separated lists (`allowedOrigins`, `sites`,
|
|
87
|
+
`maxSessionsPerIp`, `requestTimeoutMs`, `maxMessageBytes`, …), plus:
|
|
88
|
+
|
|
89
|
+
- `store`: `"sqlite"` (default), `"memory"`, or your own `TokenStore` object;
|
|
90
|
+
- `logger`: your own logger; by default JSON lines on stdout.
|
|
91
|
+
|
|
92
|
+
The returned `Relay` has `url`, `port`, `config`, `sessionCount`, `addressFor(secret)`
|
|
93
|
+
and `stop()`, which sends `bye` to every page (close code 4004) and stops the server.
|
|
94
|
+
|
|
95
|
+
The package also exports `MemoryTokenStore`, `SqliteTokenStore`, the `TokenStore` types,
|
|
96
|
+
`configFromEnv`, `resolveConfig`, `DEFAULT_CONFIG`, `createLogger`, `SUBPROTOCOL`
|
|
97
|
+
(`tab-mcp.v1`) and `VERSION`.
|
|
98
|
+
|
|
99
|
+
### Your own token store
|
|
100
|
+
|
|
101
|
+
A store keeps grants and the SHA-256 hashes of tokens, never the tokens. Check yours with
|
|
102
|
+
the conformance suite for `bun test`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { tokenStoreConformance } from "@ractive-ch/tab-mcp/testing";
|
|
106
|
+
import { MyTokenStore } from "./my-token-store.ts";
|
|
107
|
+
|
|
108
|
+
tokenStoreConformance("MyTokenStore", () => new MyTokenStore());
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
[Token store](https://github.com/ractive/tab-mcp/blob/main/docs/token-store.md) explains
|
|
112
|
+
the interface, its guarantees and a Redis example.
|
|
113
|
+
|
|
114
|
+
## Security
|
|
115
|
+
|
|
116
|
+
Connecting an AI app to a page means trusting that page, as with any MCP server. The
|
|
117
|
+
relay operator can read every tool argument and result; the relay does not log them.
|
|
118
|
+
Report vulnerabilities privately as described in
|
|
119
|
+
[SECURITY.md](https://github.com/ractive/tab-mcp/blob/main/SECURITY.md). The
|
|
120
|
+
[threat model](https://github.com/ractive/tab-mcp/blob/main/docs/threat-model.md) covers
|
|
121
|
+
the details.
|
|
122
|
+
|
|
123
|
+
## More
|
|
124
|
+
|
|
125
|
+
- [Repository and overview](https://github.com/ractive/tab-mcp)
|
|
126
|
+
- [Tab–relay protocol](https://github.com/ractive/tab-mcp/blob/main/docs/protocol.md)
|
|
127
|
+
- [Changelog](https://github.com/ractive/tab-mcp/blob/main/CHANGELOG.md)
|
|
128
|
+
|
|
129
|
+
## Licence
|
|
130
|
+
|
|
131
|
+
MIT, see [LICENSE](LICENSE) and [AI_NOTICE](AI_NOTICE).
|
package/index.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ export { SUBPROTOCOL } from "./protocol.js";
|
|
|
9
9
|
export { type CreateRelayOptions, createRelay, type Relay, VERSION, } from "./relay.js";
|
|
10
10
|
export { MemoryTokenStore } from "./store/memory.js";
|
|
11
11
|
export { SqliteTokenStore } from "./store/sqlite.js";
|
|
12
|
-
export type { GrantSummary, RevokeSelector, TokenKind, TokenRecord, TokenStore, } from "./store/types.js";
|
|
12
|
+
export type { GrantSummary, RevokeOptions, RevokeSelector, TokenKind, TokenRecord, TokenStore, } from "./store/types.js";
|
package/index.js
CHANGED
|
@@ -708,7 +708,7 @@ function parseTabMessage(text) {
|
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
710
|
// package.json
|
|
711
|
-
var version = "0.1.0
|
|
711
|
+
var version = "0.1.0";
|
|
712
712
|
|
|
713
713
|
// src/exchange.ts
|
|
714
714
|
var RelayErrorCode = {
|
|
@@ -1461,7 +1461,7 @@ function renderPage(title, body) {
|
|
|
1461
1461
|
<body>
|
|
1462
1462
|
<main>
|
|
1463
1463
|
${body}
|
|
1464
|
-
<footer>tab-mcp relay. It passes requests between AI
|
|
1464
|
+
<footer>tab-mcp relay. It passes requests between AI apps and web pages and does not store their contents.</footer>
|
|
1465
1465
|
</main>
|
|
1466
1466
|
</body>
|
|
1467
1467
|
</html>`;
|
|
@@ -1562,7 +1562,7 @@ var MESSAGES = {
|
|
|
1562
1562
|
wrong: "That code is not right. Check the code shown in the page and try again.",
|
|
1563
1563
|
expired: "That code has expired. The page now shows a new code: enter that one.",
|
|
1564
1564
|
locked: "Too many wrong codes. The page now shows a new code: enter that one.",
|
|
1565
|
-
inactive: "The page is not showing a code right now. In the page, open \u201CConnect your AI
|
|
1565
|
+
inactive: "The page is not showing a code right now. In the page, open \u201CConnect your AI app\u201D to see one, then enter it here.",
|
|
1566
1566
|
malformed: "Enter the 6 digits shown in the page.",
|
|
1567
1567
|
limited: "Too many attempts for this page. Wait a few minutes and try again. If it keeps happening, someone else may be guessing: use \u201CNew address\u201D in the page."
|
|
1568
1568
|
};
|
|
@@ -1571,7 +1571,7 @@ var codePairing = {
|
|
|
1571
1571
|
render() {
|
|
1572
1572
|
return `<p><label for="code">Code shown in the page</label>
|
|
1573
1573
|
<input id="code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9 ]{6,7}" maxlength="7" required autofocus aria-describedby="code-help"></p>
|
|
1574
|
-
<p id="code-help" class="help">In the page, \u201CConnect your AI
|
|
1574
|
+
<p id="code-help" class="help">In the page, \u201CConnect your AI app\u201D shows a 6-digit code. It changes every 2 minutes.</p>`;
|
|
1575
1575
|
},
|
|
1576
1576
|
check(target, form, admit) {
|
|
1577
1577
|
const result = target.codes.check(form.get("code") ?? "", Date.now(), admit);
|
|
@@ -1768,7 +1768,7 @@ class AuthorizationServer {
|
|
|
1768
1768
|
#errorPage(status, message) {
|
|
1769
1769
|
return this.#page(status, "This connection cannot be made", `<h1>This connection cannot be made</h1>
|
|
1770
1770
|
<p role="alert">${escapeHtml(message)}</p>
|
|
1771
|
-
<p>Go back to your AI
|
|
1771
|
+
<p>Go back to your AI app and try again. If it keeps happening, the AI app may not work with this relay.</p>`);
|
|
1772
1772
|
}
|
|
1773
1773
|
#errorUrl(redirectUri, state, error, description) {
|
|
1774
1774
|
const url = new URL(redirectUri);
|
|
@@ -1786,8 +1786,8 @@ class AuthorizationServer {
|
|
|
1786
1786
|
const url = this.#errorUrl(redirectUri, state, error, description);
|
|
1787
1787
|
const host = hostOf(redirectUri);
|
|
1788
1788
|
return this.#page(400, "This connection cannot be made", `<h1>This connection cannot be made</h1>
|
|
1789
|
-
<p role="alert">The AI
|
|
1790
|
-
<p>You can go back to the AI
|
|
1789
|
+
<p role="alert">The AI app sent a request this relay cannot accept: ${escapeHtml(description)} (${escapeHtml(error)}).</p>
|
|
1790
|
+
<p>You can go back to the AI app at ${escapeHtml(host)} to tell it, or close this page.</p>
|
|
1791
1791
|
<div class="actions"><a class="button" href="${escapeHtml(url)}">Go back to ${escapeHtml(host)}</a></div>`);
|
|
1792
1792
|
}
|
|
1793
1793
|
#redirectError(redirectUri, state, error, description) {
|
|
@@ -1937,7 +1937,7 @@ class AuthorizationServer {
|
|
|
1937
1937
|
const body = `<h1>Open the page first</h1>
|
|
1938
1938
|
${errorHtml}
|
|
1939
1939
|
<p><strong>${escapeHtml(client.name)}</strong> (${escapeHtml(client.host)}) wants to use the tools of a web page, but that page is not open right now.</p>
|
|
1940
|
-
<p>Open the page, click \u201CConnect your AI
|
|
1940
|
+
<p>Open the page, click \u201CConnect your AI app\u201D, then try again here.</p>
|
|
1941
1941
|
<div class="actions">
|
|
1942
1942
|
<a class="button primary" href="${escapeHtml(retryUrl)}">Try again</a>
|
|
1943
1943
|
<a class="button" href="${escapeHtml(cancelUrl)}">Cancel</a>
|
|
@@ -1951,7 +1951,7 @@ ${errorHtml}
|
|
|
1951
1951
|
return this.#errorPage(403, "This form can only be sent from this page.");
|
|
1952
1952
|
}
|
|
1953
1953
|
if (!this.#attemptsPerIp.take(ip)) {
|
|
1954
|
-
return this.#errorPage(429, "Too many attempts from your network. Wait a few minutes, then start again from your AI
|
|
1954
|
+
return this.#errorPage(429, "Too many attempts from your network. Wait a few minutes, then start again from your AI app.");
|
|
1955
1955
|
}
|
|
1956
1956
|
const type = (req.headers.get("content-type") ?? "").split(";")[0]?.trim();
|
|
1957
1957
|
if (type !== "application/x-www-form-urlencoded") {
|
|
@@ -2090,7 +2090,7 @@ ${errorHtml}
|
|
|
2090
2090
|
const body = `<h1>Open the page first</h1>
|
|
2091
2091
|
${errorHtml}
|
|
2092
2092
|
<p>${clientLabel} wants to use the tools of a web page, but that page is not open right now.</p>
|
|
2093
|
-
<p>Open the page, click \u201CConnect your AI
|
|
2093
|
+
<p>Open the page, click \u201CConnect your AI app\u201D, then try again here.</p>
|
|
2094
2094
|
<form method="post" action="/authorize">
|
|
2095
2095
|
${hidden}
|
|
2096
2096
|
<div class="actions">
|
|
@@ -2119,7 +2119,7 @@ ${method.render(session)}
|
|
|
2119
2119
|
${cancel}
|
|
2120
2120
|
</div>
|
|
2121
2121
|
</form>
|
|
2122
|
-
<p class="help">After you connect, your browser goes back to ${escapeHtml(hostOf(pending.redirectUri))}. Only connect AI
|
|
2122
|
+
<p class="help">After you connect, your browser goes back to ${escapeHtml(hostOf(pending.redirectUri))}. Only connect AI apps you started yourself.</p>`;
|
|
2123
2123
|
return this.#page(status, heading, body, targets, extra);
|
|
2124
2124
|
}
|
|
2125
2125
|
async token(req, ip) {
|
|
@@ -3332,7 +3332,7 @@ function startRelay(options, hooks) {
|
|
|
3332
3332
|
const session = sessions.get(addressId);
|
|
3333
3333
|
if (session === undefined || !session.ready) {
|
|
3334
3334
|
logDone(503, method, { reason: "page_not_open" });
|
|
3335
|
-
return rpcError(503, rpcId, RelayErrorCode.pageNotOpen, 'The page is not open. Open the web page that offers these tools and click "Connect your AI
|
|
3335
|
+
return rpcError(503, rpcId, RelayErrorCode.pageNotOpen, 'The page is not open. Open the web page that offers these tools and click "Connect your AI app".', corsHeaders);
|
|
3336
3336
|
}
|
|
3337
3337
|
if (session.pending.size >= session.limits.maxConcurrentRequests) {
|
|
3338
3338
|
logDone(429, method);
|
|
@@ -3554,5 +3554,5 @@ export {
|
|
|
3554
3554
|
resolveConfig
|
|
3555
3555
|
};
|
|
3556
3556
|
|
|
3557
|
-
//# debugId=
|
|
3557
|
+
//# debugId=0BEEC7645167157064756E2164756E21
|
|
3558
3558
|
//# sourceMappingURL=index.js.map
|