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 +17 -0
- package/package.json +1 -1
- package/src/mcp-http.js +75 -0
- package/src/shorten.js +4 -2
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
package/src/mcp-http.js
ADDED
|
@@ -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
|
|
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 }),
|