jotview 0.3.0 → 0.4.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 CHANGED
@@ -107,6 +107,23 @@ claude mcp add jotview -- npx -y jotview mcp --raw
107
107
 
108
108
  Works with clients on any MCP revision from `2024-11-05` through `2026-07-28`.
109
109
 
110
+ ### Remote server — no install
111
+
112
+ The same tool runs at **`https://mcp.jotbins.com/mcp`** (Streamable HTTP, no
113
+ sign-in), for clients that can't run local commands.
114
+
115
+ - **claude.ai (web and mobile):** Settings → Connectors → Add custom
116
+ connector → paste `https://mcp.jotbins.com/mcp`.
117
+ - **Claude Code:** `claude mcp add -s user --transport http jotview https://mcp.jotbins.com/mcp`
118
+
119
+ Same rules as the local server, except it can't forbid uploads: use
120
+ `shorten: "never"` per call, or the local `--raw` server, to keep a document
121
+ off `surl.jotbins.com`. Uploads are rate-limited per client; over the limit
122
+ you still get the full link.
123
+
124
+ The Worker lives in [`worker/`](worker/) and deploys with
125
+ `cd worker && npx wrangler deploy`.
126
+
110
127
  ## Output contract
111
128
 
112
129
  **stdout is the URL and nothing else.** Notices, warnings, and errors go to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jotview",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Turn markdown into a jotbins /d link — a CLI and an MCP server. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,75 @@
1
+ import { handleMessage, parseError, LEGACY_VERSIONS, MODERN_VERSIONS } from "./mcp.js";
2
+
3
+ /** Docs cap at 1 MB (MAX_DOC_BYTES); leave room for JSON escaping. */
4
+ export const MAX_BODY_BYTES = 2_000_000;
5
+
6
+ const KNOWN_VERSIONS = [...LEGACY_VERSIONS, ...MODERN_VERSIONS];
7
+
8
+ // No cookies or credentials are involved, so any origin may call — that lets
9
+ // browser-based clients such as MCP Inspector connect directly.
10
+ const CORS = {
11
+ "access-control-allow-origin": "*",
12
+ "access-control-allow-methods": "POST, OPTIONS",
13
+ "access-control-allow-headers": "content-type, accept, mcp-protocol-version, mcp-session-id",
14
+ "access-control-max-age": "86400",
15
+ };
16
+
17
+ /** @param {unknown} body @param {number} [status] */
18
+ const json = (body, status = 200) =>
19
+ new Response(JSON.stringify(body), { status, headers: { ...CORS, "content-type": "application/json" } });
20
+
21
+ /** @param {string} text @param {number} status @param {Record<string,string>} [headers] */
22
+ const plain = (text, status, headers = {}) =>
23
+ new Response(text, { status, headers: { ...CORS, "content-type": "text/plain; charset=utf-8", ...headers } });
24
+
25
+ /** @param {string} endpoint */
26
+ const landing = (endpoint) => `jotview — an MCP server that turns markdown into rendered jotbins links.
27
+
28
+ MCP endpoint (Streamable HTTP, no sign-in):
29
+ ${endpoint}
30
+
31
+ claude.ai: Settings → Connectors → Add custom connector → paste the URL above.
32
+ Claude Code: claude mcp add -s user --transport http jotview ${endpoint}
33
+
34
+ Source: https://github.com/jotbins/jotview
35
+ `;
36
+
37
+ /**
38
+ * Serve MCP over Streamable HTTP. Every request gets exactly one JSON reply —
39
+ * no SSE stream and no sessions, since the only tool answers immediately.
40
+ *
41
+ * @param {Request} request
42
+ * @param {import("./mcp.js").Context} ctx
43
+ * @returns {Promise<Response>}
44
+ */
45
+ export async function handleHttp(request, ctx) {
46
+ const url = new URL(request.url);
47
+ if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
48
+
49
+ if (url.pathname === "/") {
50
+ if (request.method !== "GET") return plain("Method not allowed\n", 405, { allow: "GET" });
51
+ return plain(landing(`${url.origin}/mcp`), 200);
52
+ }
53
+ if (url.pathname !== "/mcp") return plain("Not found\n", 404);
54
+ if (request.method !== "POST") return plain("Method not allowed\n", 405, { allow: "POST, OPTIONS" });
55
+
56
+ const version = request.headers.get("mcp-protocol-version");
57
+ if (version !== null && !KNOWN_VERSIONS.includes(version)) {
58
+ return plain(`Unsupported MCP-Protocol-Version: ${version}\n`, 400);
59
+ }
60
+
61
+ const declared = Number(request.headers.get("content-length"));
62
+ if (declared > MAX_BODY_BYTES) return plain("Request too large\n", 413);
63
+ const bytes = await request.arrayBuffer();
64
+ if (bytes.byteLength > MAX_BODY_BYTES) return plain("Request too large\n", 413);
65
+
66
+ let msg;
67
+ try {
68
+ msg = JSON.parse(new TextDecoder().decode(bytes));
69
+ } catch {
70
+ return json(parseError(), 400);
71
+ }
72
+
73
+ const reply = await handleMessage(msg, ctx);
74
+ return reply ? json(reply) : new Response(null, { status: 202, headers: CORS });
75
+ }
package/src/shorten.js CHANGED
@@ -28,15 +28,17 @@ export function shouldShorten(url, opts) {
28
28
  /**
29
29
  * Shorten a URL. Never throws — the caller falls back to the raw URL.
30
30
  * @param {string} url
31
- * @param {{endpoint?: string, permanent?: boolean}} [options]
31
+ * @param {{endpoint?: string, permanent?: boolean, fetchImpl?: typeof fetch}} [options]
32
32
  * `permanent` asks surl for a link that never expires. Left out of the body
33
33
  * when false, so the request matches what older surl versions expect.
34
+ * `fetchImpl` lets the Worker route through its surl service binding.
34
35
  * @returns {Promise<{ok: true, short: string} | {ok: false, reason: string}>}
35
36
  */
36
37
  export async function shorten(url, options = {}) {
37
38
  const endpoint = options.endpoint ?? SHORTENER_URL;
39
+ const doFetch = options.fetchImpl ?? fetch;
38
40
  try {
39
- const response = await fetch(endpoint, {
41
+ const response = await doFetch(endpoint, {
40
42
  method: "POST",
41
43
  headers: { "content-type": "application/json" },
42
44
  body: JSON.stringify(options.permanent ? { url, permanent: true } : { url }),