bowmark-mcp 2.0.0 → 2.0.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
@@ -34,7 +34,7 @@ If your host speaks streamable HTTP, skip this bridge and connect directly to
34
34
 
35
35
  | Var | Meaning |
36
36
  |---|---|
37
- | `BOWMARK_MCP_URL` | Target MCP URL. Default `https://api.bowmark.ai/mcp?s=n` (`?s=n` attributes the install to the npm bridge). Point at `http://localhost:3001/mcp` for a local Bowmark API. |
37
+ | `BOWMARK_MCP_URL` | Target MCP URL. Default `https://api.bowmark.ai/mcp/npm` (the `/npm` destination attributes the install to this bridge). Point at `http://localhost:3001/mcp` for a local Bowmark API. |
38
38
  | `BOWMARK_API_KEY` | Optional. Forwarded as `X-Bowmark-Key`; a free key (bowmark.ai dashboard) lifts the anonymous per-IP daily cap to your plan budget. |
39
39
 
40
40
  ## Design notes (repo-internal)
@@ -44,13 +44,24 @@ If your host speaks streamable HTTP, skip this bridge and connect directly to
44
44
  initialize round-trip) is noise next to a `run` that drives real browsers — and it
45
45
  sidesteps long-lived-connection failure modes without reconnect bookkeeping.
46
46
  Mirrors `packages/bowmark-mcp/python` (the PyPI bridge) exactly.
47
+ - **The real host's name is relayed upstream.** The api tailors its operating
48
+ guidance per host and detects the host from `clientInfo` on the handshake —
49
+ but through a bridge that names the BRIDGE, so every install here would read
50
+ the platform-neutral text. So the host's own `clientInfo.name` (from OUR stdio
51
+ handshake) is forwarded as `X-Bowmark-Client` on every proxied request, and the
52
+ api ranks that above its own handshake. Best-effort: an unknown or missing name
53
+ simply falls back to the neutral text, never an error.
47
54
  - **Pass-through only.** No tool logic lives here; the agent-surfaces sync
48
55
  rule in the root CLAUDE.md is unaffected because descriptions/schemas ride
49
56
  through from `apps/api/src/routes/mcp.ts`. `callTool` results are returned
50
57
  verbatim (content, structuredContent, isError).
51
- - **`?s=n` source code** is registered in `apps/api/src/mcp-sources.ts` +
52
- `mcp-registry/sources.json` (npm stdio bridge channel; the PyPI bridge is
53
- `?s=p`).
58
+ - **The `/npm` destination** is registered in
59
+ `apps/api/src/mcp-destinations.ts` + `mcp-registry/sources.json` (npm stdio
60
+ bridge channel; the PyPI bridge is `/pypi`). It carries the channel and
61
+ deliberately pins NO platform, because the relayed `X-Bowmark-Client` above is
62
+ a better answer than any pin. The old `?s=n` query form still resolves for
63
+ installs already in the wild. Adding a destination:
64
+ `.claude/rules/mcp-destinations.md`.
54
65
  - **npm ownership verification needs BOTH markers — the registry checks the
55
66
  published package.json, not just the README.** The `mcp-name:
56
67
  ai.bowmark/bowmark` line above and the `"mcpName": "ai.bowmark/bowmark"`
package/dist/bridge.js CHANGED
@@ -8,9 +8,9 @@
8
8
  // package never needs a re-publish when the api's tools change.
9
9
  //
10
10
  // Env:
11
- // BOWMARK_MCP_URL Target MCP URL. Default https://api.bowmark.ai/mcp?s=n
12
- // (`?s=n` tags the install source as the npm bridge;
13
- // point at http://localhost:3001/mcp for a local API).
11
+ // BOWMARK_MCP_URL Target MCP URL. Default https://api.bowmark.ai/mcp/npm
12
+ // (the `/npm` destination attributes the install to this
13
+ // bridge; point at http://localhost:3001/mcp for a local API).
14
14
  // BOWMARK_API_KEY Optional key, forwarded as X-Bowmark-Key. Omit for the
15
15
  // anonymous tier.
16
16
  //
@@ -24,7 +24,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
24
24
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
25
25
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
26
26
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
27
- export const DEFAULT_URL = "https://api.bowmark.ai/mcp?s=n";
27
+ export const DEFAULT_URL = "https://api.bowmark.ai/mcp/npm";
28
28
  export function targetUrl(env = process.env) {
29
29
  return env.BOWMARK_MCP_URL?.trim() || DEFAULT_URL;
30
30
  }
@@ -34,11 +34,26 @@ export function authHeaders(env = process.env) {
34
34
  const key = env.BOWMARK_API_KEY?.trim();
35
35
  return key ? { "X-Bowmark-Key": key } : undefined;
36
36
  }
37
- async function withRemote(fn) {
38
- const client = new Client({ name: "bowmark-mcp-bridge", version: "2.0.0" });
39
- const headers = authHeaders();
37
+ /** Relay WHICH HOST is really on the other end.
38
+ *
39
+ * The hosted MCP tailors its instructions per host (ChatGPT vs a chat app vs a
40
+ * coding agent), and it detects the host from the `clientInfo` on the
41
+ * handshake. Through this bridge that signal is destroyed: we open our OWN
42
+ * session upstream, so the name the api would see is `bowmark-mcp-bridge` and
43
+ * every install through npm resolves to the platform-neutral text.
44
+ *
45
+ * The real host named itself on OUR stdio handshake, so relay that name on
46
+ * every proxied request. Best-effort: `undefined` before the host has
47
+ * initialized, and the api treats an unrecognized name the same as none. */
48
+ export function clientHeaders(hostName) {
49
+ const name = hostName?.trim();
50
+ return name ? { "X-Bowmark-Client": name } : undefined;
51
+ }
52
+ async function withRemote(fn, hostName) {
53
+ const client = new Client({ name: "bowmark-mcp-bridge", version: "2.0.1" });
54
+ const headers = { ...authHeaders(), ...clientHeaders(hostName) };
40
55
  const transport = new StreamableHTTPClientTransport(new URL(targetUrl()), {
41
- requestInit: headers ? { headers } : undefined,
56
+ requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
42
57
  });
43
58
  await client.connect(transport);
44
59
  try {
@@ -50,24 +65,56 @@ async function withRemote(fn) {
50
65
  }
51
66
  /** One retry on any failure: the remote is stateless, so a fresh session is
52
67
  * equivalent, and a transient network blip shouldn't fail the host's call. */
53
- export async function callRemote(fn, connect = withRemote) {
68
+ export async function callRemote(fn, connect = withRemote, hostName) {
54
69
  try {
55
- return await connect(fn);
70
+ return await connect(fn, hostName);
56
71
  }
57
72
  catch (first) {
58
73
  console.error(`bowmark-mcp: retrying after: ${String(first)}`);
59
- return await connect(fn);
74
+ return await connect(fn, hostName);
75
+ }
76
+ }
77
+ /** The hosted server's `instructions` string, fetched with one initialize.
78
+ *
79
+ * Instructions are handed to the host at OUR initialize, which happens before we
80
+ * have talked to the remote — so unlike tools they cannot be lazily proxied per
81
+ * request. Fetching once at startup keeps the hosted server the single source of
82
+ * truth (the alternative, a hardcoded copy here, would drift the moment the api
83
+ * changed it and could only be corrected by an npm re-publish).
84
+ *
85
+ * KNOWN LIMITATION, and it is the reason this is not a bug report: the api tailors
86
+ * its instructions PER HOST, but this fetch happens before any host has connected
87
+ * to us, so we cannot relay a host name yet and the api answers with its
88
+ * platform-neutral text. Tool descriptions, fetched per request once the host HAS
89
+ * identified itself, are correctly per-host. Pinning a destination in
90
+ * BOWMARK_MCP_URL (e.g. .../mcp/cursor) is the way to get tailored instructions
91
+ * through a bridge.
92
+ *
93
+ * Returns undefined on any failure: instructions are a ranking hint, and a
94
+ * bridge that refused to start because a hint was unavailable would be strictly
95
+ * worse than one that starts without it. */
96
+ export async function fetchInstructions(remote = callRemote) {
97
+ try {
98
+ return await remote(async (c) => c.getInstructions());
99
+ }
100
+ catch (err) {
101
+ console.error(`bowmark-mcp: could not read server instructions: ${String(err)}`);
102
+ return undefined;
60
103
  }
61
104
  }
62
- export function buildServer(remote = callRemote) {
63
- const server = new Server({ name: "bowmark", version: "2.0.0" }, { capabilities: { tools: {} } });
105
+ export function buildServer(remote = callRemote, instructions) {
106
+ const server = new Server({ name: "bowmark", version: "2.0.1" }, { capabilities: { tools: {} }, instructions });
107
+ // Whoever ran us said who they are on the stdio handshake. Read it per
108
+ // request rather than once: `tools/list` can arrive before the SDK has
109
+ // recorded it, and a fresh read costs nothing.
110
+ const hostName = () => server.getClientVersion()?.name;
64
111
  server.setRequestHandler(ListToolsRequestSchema, async () => {
65
- return await remote((c) => c.listTools());
112
+ return await remote((c) => c.listTools(), undefined, hostName());
66
113
  });
67
114
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
68
115
  // Returned verbatim — content, structuredContent, and isError all ride
69
116
  // through, so upstream errors reach the host exactly as the api sent them.
70
- return await remote((c) => c.callTool({ name: req.params.name, arguments: req.params.arguments ?? {} }));
117
+ return await remote((c) => c.callTool({ name: req.params.name, arguments: req.params.arguments ?? {} }), undefined, hostName());
71
118
  });
72
119
  return server;
73
120
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // stdout belongs to the protocol; anything human goes to stderr.
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { buildServer } from "./bridge.js";
5
- const server = buildServer();
4
+ import { buildServer, callRemote, fetchInstructions } from "./bridge.js";
5
+ // One initialize before we serve, purely to carry the hosted server's
6
+ // `instructions` through to the host — they ride on OUR initialize, so they
7
+ // cannot be proxied per request the way tools are. Never fatal; see
8
+ // fetchInstructions.
9
+ const server = buildServer(callRemote, await fetchInstructions());
6
10
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bowmark-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "mcpName": "ai.bowmark/bowmark",
5
5
  "description": "Bowmark MCP over stdio \u2014 a thin bridge to the hosted Bowmark MCP at https://api.bowmark.ai/mcp. The web as callable functions: read a typed library, write a script, Bowmark runs it on the live sites.",
6
6
  "license": "MIT",