@ractive-ch/tab-mcp 0.6.0 → 0.6.1

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 CHANGED
@@ -10,7 +10,7 @@ and gets an MCP address. The user adds that address to an AI app and pairs it by
10
10
  a code from the page on the relay's sign-in page. The relay then forwards the AI app's
11
11
  requests to the page. It knows nothing about the tools themselves.
12
12
 
13
- **Status: 0.6.0.** Until 1.0, a new minor version can still change settings and the API.
13
+ **Status: 0.6.1.** Until 1.0, a new minor version can still change settings and the API.
14
14
  The relay speaks MCP revision 2026-07-28 only and accepts AI apps that register through
15
15
  OAuth Client ID Metadata Documents; see [compatibility](https://github.com/ractive/tab-mcp/blob/main/docs/compatibility.md)
16
16
  for which apps work.
@@ -32,7 +32,7 @@ Or with Docker (linux/amd64 and linux/arm64):
32
32
  docker run -d -p 127.0.0.1:8080:8080 -v tab-mcp-data:/data \
33
33
  -e TAB_MCP_PUBLIC_URL=https://relay.example.com \
34
34
  -e TAB_MCP_ALLOWED_ORIGINS=https://app.example.com \
35
- ractive/tab-mcp:0.6.0
35
+ ractive/tab-mcp:0.6.1
36
36
  ```
37
37
 
38
38
  The image is also on `ghcr.io/ractive/tab-mcp`. Put the relay behind a reverse proxy
@@ -112,6 +112,25 @@ tokenStoreConformance("MyTokenStore", () => new MyTokenStore());
112
112
  [Token store](https://github.com/ractive/tab-mcp/blob/main/docs/token-store.md) explains
113
113
  the interface, its guarantees and a Redis example.
114
114
 
115
+ ### Testing a page's tools
116
+
117
+ For end-to-end tests under `bun test`, `startTestRelay()` runs a relay in the test
118
+ process (127.0.0.1, a free port, development mode, every page origin allowed, a memory
119
+ store, no log output unless `logger` or `logLevel` is given; options override any of that), and `tokenFor()` writes a token for
120
+ a page's MCP address into its store, as if an AI app had paired:
121
+
122
+ ```ts
123
+ import { startTestRelay, tokenFor } from "@ractive-ch/tab-mcp/testing";
124
+
125
+ const relay = startTestRelay();
126
+ // The page connects with `relay: relay.url`; its address is `session.address`.
127
+ const token = await tokenFor(relay.store, address);
128
+ // Any MCP client, with the header `Authorization: Bearer ${token}`.
129
+ await relay.stop();
130
+ ```
131
+
132
+ It is for tests only: it accepts every origin and skips pairing.
133
+
115
134
  ## Security
116
135
 
117
136
  Connecting an AI app to a page means trusting that page, as with any MCP server. The
package/index.js CHANGED
@@ -1218,7 +1218,7 @@ function parseTabMessage(text) {
1218
1218
  }
1219
1219
  }
1220
1220
  // package.json
1221
- var version = "0.6.0";
1221
+ var version = "0.6.1";
1222
1222
 
1223
1223
  // src/exchange.ts
1224
1224
  var RelayErrorCode = {
@@ -1848,6 +1848,26 @@ function newId(prefix) {
1848
1848
  function hashToken(token) {
1849
1849
  return new Bun.CryptoHasher("sha256").update(token).digest().toBase64({ alphabet: "base64url", omitPadding: true });
1850
1850
  }
1851
+ async function issueAccessToken(store, grant, options) {
1852
+ const now = options.now ?? Date.now();
1853
+ const token = newToken("access");
1854
+ const record = {
1855
+ hash: hashToken(token),
1856
+ kind: "access",
1857
+ grantId: grant.grantId ?? newId("g"),
1858
+ addressId: grant.addressId,
1859
+ clientId: grant.clientId,
1860
+ clientName: grant.clientName,
1861
+ scopes: grant.scopes ?? ["tools"],
1862
+ grantCreatedAt: grant.grantCreatedAt ?? now,
1863
+ issuedAt: now,
1864
+ expiresAt: now + options.ttlMs
1865
+ };
1866
+ if (grant.subject !== undefined)
1867
+ record.subject = grant.subject;
1868
+ await store.put(record);
1869
+ return { token, record };
1870
+ }
1851
1871
  async function issueTokenPair(store, grant, options) {
1852
1872
  const now = options.now ?? Date.now();
1853
1873
  const accessToken = newToken("access");
@@ -4845,6 +4865,13 @@ var FORWARDED_HEADERS = new Set([
4845
4865
  "traceparent",
4846
4866
  "tracestate"
4847
4867
  ]);
4868
+ function developmentUrl(host, port) {
4869
+ if (host === "127.0.0.1")
4870
+ return `http://127.0.0.1:${port}`;
4871
+ if (host === "::1" || host === "[::1]")
4872
+ return `http://[::1]:${port}`;
4873
+ return `http://localhost:${port}`;
4874
+ }
4848
4875
  var HEALTH_STORE_TIMEOUT_MS = 1000;
4849
4876
  var HEALTH_REUSE_MS = 1000;
4850
4877
  var HEALTH_HUNG_RETRY_MS = 30000;
@@ -5711,7 +5738,7 @@ function startRelay(options, hooks) {
5711
5738
  let closing = false;
5712
5739
  const port = server.port ?? config.port;
5713
5740
  if (publicUrl === "")
5714
- publicUrl = `http://localhost:${port}`;
5741
+ publicUrl = developmentUrl(config.host, port);
5715
5742
  const sweep = setInterval(() => {
5716
5743
  auth.sweep();
5717
5744
  store.deleteExpired(Date.now()).then((n) => {
@@ -5784,5 +5811,5 @@ export {
5784
5811
  resolveConfig
5785
5812
  };
5786
5813
 
5787
- //# debugId=61513056506259C164756E2164756E21
5814
+ //# debugId=F0E5B49004089DE664756E2164756E21
5788
5815
  //# sourceMappingURL=index.js.map