whamlink-mcp 0.1.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 +49 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +47 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# whamlink-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for [whamlink](https://whamlink.com) — let any MCP client (Claude Desktop, IDEs, agents) publish a single file to a permanent, shareable link.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Get an API key: register at <https://whamlink.com/app>, then create a key under **Keys**.
|
|
8
|
+
2. Add the server to your MCP client config, passing the key via `WHAMLINK_API_KEY`.
|
|
9
|
+
|
|
10
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"whamlink": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "whamlink-mcp"],
|
|
18
|
+
"env": { "WHAMLINK_API_KEY": "wl_your_key_here" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Running from source instead of npm: `"command": "node", "args": ["/path/to/whamlink/mcp/dist/index.js"]` (after `npm install && npm run build` in `mcp/`).
|
|
25
|
+
|
|
26
|
+
### Environment
|
|
27
|
+
|
|
28
|
+
- `WHAMLINK_API_KEY` (required) — your whamlink API key.
|
|
29
|
+
- `WHAMLINK_BASE_URL` (optional) — defaults to `https://whamlink.com`.
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
| Tool | What it does |
|
|
34
|
+
|------|--------------|
|
|
35
|
+
| `publish_link` | Publish HTML / Markdown / PDF / image / text → a permanent URL. Public by default; set `visibility` to `private` / `password` / `email` to gate it. |
|
|
36
|
+
| `list_links` | List your published links (id, slug, mode, visibility, URL). |
|
|
37
|
+
| `set_link_access` | Change a link's visibility, password, shared-email list, `allowNetwork`, or title. |
|
|
38
|
+
| `replace_link_content` | Replace a link's content in place — the URL stays the same. |
|
|
39
|
+
| `delete_link` | Permanently delete a link. |
|
|
40
|
+
|
|
41
|
+
**Never publish secrets, API keys, or private data.** Public links are unlisted but anyone with the URL can view them; use `private`/`password`/`email` visibility for anything sensitive.
|
|
42
|
+
|
|
43
|
+
## Develop
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install
|
|
47
|
+
npm run build
|
|
48
|
+
npm test
|
|
49
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface WhamlinkConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
fetchImpl?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export declare class WhamlinkError extends Error {
|
|
7
|
+
code: string;
|
|
8
|
+
status: number;
|
|
9
|
+
constructor(code: string, message: string, status: number);
|
|
10
|
+
}
|
|
11
|
+
export type Visibility = "public" | "private" | "password" | "email";
|
|
12
|
+
export type Mode = "sandboxed_html" | "sanitized_html" | "markdown" | "pdf" | "image" | "text";
|
|
13
|
+
export interface PublishArgs {
|
|
14
|
+
mode: Mode;
|
|
15
|
+
content: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
allowNetwork?: boolean;
|
|
18
|
+
visibility?: Visibility;
|
|
19
|
+
password?: string;
|
|
20
|
+
shareEmails?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface AccessArgs {
|
|
23
|
+
visibility?: Visibility;
|
|
24
|
+
password?: string;
|
|
25
|
+
shareEmails?: string[];
|
|
26
|
+
allowNetwork?: boolean;
|
|
27
|
+
title?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const publishLink: (cfg: WhamlinkConfig, args: PublishArgs) => Promise<any>;
|
|
30
|
+
export declare const listLinks: (cfg: WhamlinkConfig) => Promise<any>;
|
|
31
|
+
export declare const deleteLink: (cfg: WhamlinkConfig, id: string) => Promise<any>;
|
|
32
|
+
export declare const setLinkAccess: (cfg: WhamlinkConfig, id: string, args: AccessArgs) => Promise<any>;
|
|
33
|
+
export declare const replaceLinkContent: (cfg: WhamlinkConfig, id: string, args: {
|
|
34
|
+
mode: Mode;
|
|
35
|
+
content: string;
|
|
36
|
+
}) => Promise<any>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Thin, testable wrapper over the whamlink HTTP API. API-key auth — no CSRF header needed
|
|
2
|
+
// (the app only requires x-whamlink-app on cookie-authenticated requests).
|
|
3
|
+
export class WhamlinkError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(code, message, status) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "WhamlinkError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function call(cfg, method, path, body) {
|
|
14
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
15
|
+
const res = await f(`${cfg.baseUrl}${path}`, {
|
|
16
|
+
method,
|
|
17
|
+
headers: { "x-api-key": cfg.apiKey, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
|
|
18
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
19
|
+
});
|
|
20
|
+
if (res.status === 204)
|
|
21
|
+
return undefined;
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
const json = text ? JSON.parse(text) : undefined;
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw new WhamlinkError(json?.error?.code ?? "error", json?.error?.message ?? `Request failed (${res.status})`, res.status);
|
|
26
|
+
}
|
|
27
|
+
return json;
|
|
28
|
+
}
|
|
29
|
+
export const publishLink = (cfg, args) => call(cfg, "POST", "/v1/publish", args);
|
|
30
|
+
export const listLinks = (cfg) => call(cfg, "GET", "/v1/docs");
|
|
31
|
+
export const deleteLink = (cfg, id) => call(cfg, "DELETE", `/v1/docs/${encodeURIComponent(id)}`);
|
|
32
|
+
export const setLinkAccess = (cfg, id, args) => call(cfg, "PATCH", `/v1/docs/${encodeURIComponent(id)}`, args);
|
|
33
|
+
export const replaceLinkContent = (cfg, id, args) => call(cfg, "PUT", `/v1/docs/${encodeURIComponent(id)}/content`, args);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { WhamlinkError, publishLink, listLinks, deleteLink, setLinkAccess, replaceLinkContent, } from "./client.js";
|
|
6
|
+
const apiKey = process.env.WHAMLINK_API_KEY;
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
process.stderr.write("whamlink-mcp: WHAMLINK_API_KEY is required. Get a key by registering at https://whamlink.com/app and pasting it into your MCP client config.\n");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const cfg = { baseUrl: process.env.WHAMLINK_BASE_URL ?? "https://whamlink.com", apiKey };
|
|
12
|
+
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
13
|
+
async function guard(fn, render) {
|
|
14
|
+
try {
|
|
15
|
+
return ok(render(await fn()));
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
const msg = e instanceof WhamlinkError ? `whamlink error (${e.status} ${e.code}): ${e.message}` : `whamlink request failed: ${e.message}`;
|
|
19
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const visibility = z.enum(["public", "private", "password", "email"]);
|
|
23
|
+
const mode = z.enum(["sandboxed_html", "sanitized_html", "markdown", "pdf", "image", "text"]);
|
|
24
|
+
const server = new McpServer({ name: "whamlink", version: "0.1.0" });
|
|
25
|
+
server.tool("publish_link", "Publish one file (HTML, Markdown, PDF as base64, image as base64, or text) to a permanent whamlink URL. Links are public (unlisted) by default; set visibility to private/password/email to gate them. Never publish secrets or private data.", {
|
|
26
|
+
mode: mode.describe("sandboxed_html (runs JS, isolated), sanitized_html (scripts stripped), markdown, pdf, image, or text"),
|
|
27
|
+
content: z.string().describe("The file content. For pdf/image, base64 is accepted via the API's text path only for small files; prefer text/markdown/html here."),
|
|
28
|
+
title: z.string().optional(),
|
|
29
|
+
allowNetwork: z.boolean().optional().describe("Let sandboxed/sanitized HTML load https CDN scripts/styles"),
|
|
30
|
+
visibility: visibility.optional().describe("Default public. private = owner only; password = also pass `password`; email = also pass `shareEmails`"),
|
|
31
|
+
password: z.string().min(6).optional(),
|
|
32
|
+
shareEmails: z.array(z.string().email()).optional(),
|
|
33
|
+
}, (a) => guard(() => publishLink(cfg, a), (r) => `Published: ${r.url}\n(id: ${r.id}, slug: ${r.slug})`));
|
|
34
|
+
server.tool("list_links", "List the links you've published (id, slug, title, mode, visibility, size, URL).", {}, () => guard(() => listLinks(cfg), (r) => !r.docs?.length ? "No links yet." :
|
|
35
|
+
r.docs.map((d) => `• ${d.title || d.slug} — ${cfg.baseUrl}/${d.slug} [${d.mode}, ${d.visibility ?? "public"}] (id: ${d.id})`).join("\n")));
|
|
36
|
+
server.tool("delete_link", "Permanently delete a published link by its id.", { id: z.string().describe("The doc id, e.g. doc_xxx (from list_links or publish_link)") }, (a) => guard(() => deleteLink(cfg, a.id), () => `Deleted ${a.id}.`));
|
|
37
|
+
server.tool("set_link_access", "Change a link's access: visibility (public/private/password/email), password, shared email list, allowNetwork, or title.", {
|
|
38
|
+
id: z.string(),
|
|
39
|
+
visibility: visibility.optional(),
|
|
40
|
+
password: z.string().min(6).optional(),
|
|
41
|
+
shareEmails: z.array(z.string().email()).optional(),
|
|
42
|
+
allowNetwork: z.boolean().optional(),
|
|
43
|
+
title: z.string().optional(),
|
|
44
|
+
}, ({ id, ...rest }) => guard(() => setLinkAccess(cfg, id, rest), () => `Updated access for ${id}.`));
|
|
45
|
+
server.tool("replace_link_content", "Replace a link's content in place — the URL stays the same.", { id: z.string(), mode, content: z.string() }, ({ id, mode, content }) => guard(() => replaceLinkContent(cfg, id, { mode, content }), (r) => `Replaced content for ${id} (${r.url}).`));
|
|
46
|
+
await server.connect(new StdioServerTransport());
|
|
47
|
+
process.stderr.write(`whamlink-mcp connected (base: ${cfg.baseUrl})\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whamlink-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for whamlink — publish a single file to a permanent, shareable link from any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"whamlink-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"whamlink",
|
|
24
|
+
"publish",
|
|
25
|
+
"model-context-protocol"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
30
|
+
"zod": "^3.23.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^26.0.1",
|
|
34
|
+
"typescript": "^5.5.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|