@ractive-ch/tab-mcp 0.1.0-alpha.1 → 0.2.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/config.d.ts +7 -0
- package/exchange.d.ts +23 -0
- package/index.d.ts +1 -1
- package/index.js +268 -49
- package/index.js.map +11 -10
- package/main.js +437 -215
- package/main.js.map +13 -12
- package/notopen.d.ts +24 -0
- package/package.json +1 -1
- package/session.d.ts +5 -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.2.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.2.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/config.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface Site {
|
|
|
45
45
|
* chunk (below 896 bytes).
|
|
46
46
|
*/
|
|
47
47
|
export declare const MIN_MESSAGE_BYTES = 16384;
|
|
48
|
+
/** Longest grace period for a page to (re)connect (DEC-074). */
|
|
49
|
+
export declare const MAX_PAGE_WAIT_MS = 60000;
|
|
48
50
|
/** Longest app name (configured or sent by the page). */
|
|
49
51
|
export declare const MAX_APP_NAME = 64;
|
|
50
52
|
export interface RelayConfig {
|
|
@@ -77,6 +79,11 @@ export interface RelayConfig {
|
|
|
77
79
|
helloTimeoutMs: number;
|
|
78
80
|
/** Deadline for receiving an MCP request body (Bun's idle timer does not cover it). */
|
|
79
81
|
bodyTimeoutMs: number;
|
|
82
|
+
/**
|
|
83
|
+
* How long an MCP request waits for its page to (re)connect before the relay
|
|
84
|
+
* answers "the page is not open" (DEC-074); 0 answers at once.
|
|
85
|
+
*/
|
|
86
|
+
pageWaitMs: number;
|
|
80
87
|
/** Limit for MCP request bodies and for messages from the tab. */
|
|
81
88
|
maxMessageBytes: number;
|
|
82
89
|
accessTokenTtlMs: number;
|
package/exchange.d.ts
CHANGED
|
@@ -17,6 +17,18 @@ export declare const RelayErrorCode: {
|
|
|
17
17
|
readonly storeUnavailable: -31008;
|
|
18
18
|
};
|
|
19
19
|
export type CancelReason = "client_closed" | "timeout" | "error";
|
|
20
|
+
/**
|
|
21
|
+
* What to do when the page goes away mid-request (DEC-074): send the request
|
|
22
|
+
* to the page again once it is back (only while nothing was sent to the AI
|
|
23
|
+
* app), or answer with this JSON-RPC message at HTTP 200 (`undefined` for a
|
|
24
|
+
* notification: then 503).
|
|
25
|
+
*/
|
|
26
|
+
export type PageGoneReply = {
|
|
27
|
+
retry: true;
|
|
28
|
+
} | {
|
|
29
|
+
retry: false;
|
|
30
|
+
body?: string;
|
|
31
|
+
};
|
|
20
32
|
export interface ExchangeOptions {
|
|
21
33
|
id: string;
|
|
22
34
|
/** JSON-RPC id of the request; `undefined` for a notification. */
|
|
@@ -36,6 +48,11 @@ export interface ExchangeOptions {
|
|
|
36
48
|
cancel: (reason: CancelReason) => void;
|
|
37
49
|
/** Called once when the exchange is over. */
|
|
38
50
|
onDone: (exchange: Exchange) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Decides the answer when the page goes away mid-request; `waiting` is true
|
|
53
|
+
* while nothing was sent to the AI app. Without it: 503, `-31003`.
|
|
54
|
+
*/
|
|
55
|
+
pageGone?: (waiting: boolean) => PageGoneReply;
|
|
39
56
|
}
|
|
40
57
|
/** One SSE `message` event carrying `data` (split into `data:` lines). */
|
|
41
58
|
export declare function sseEvent(data: string): string;
|
|
@@ -60,6 +77,12 @@ export declare class Exchange {
|
|
|
60
77
|
onEnd(): void;
|
|
61
78
|
/** The AI app went away: tell the tab, send nothing more. */
|
|
62
79
|
clientClosed(): void;
|
|
80
|
+
/**
|
|
81
|
+
* The page's session ended. With a retry the exchange ends without an
|
|
82
|
+
* answer (its `response` never settles; the relay sends the request again),
|
|
83
|
+
* else the AI app gets the `pageGone` answer at HTTP 200 (DEC-074).
|
|
84
|
+
*/
|
|
85
|
+
tabGone(): void;
|
|
63
86
|
/**
|
|
64
87
|
* End the exchange with an error the AI app can read: an HTTP error while
|
|
65
88
|
* nothing was sent, else a JSON-RPC error event if the stream is at an
|
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";
|