whamlink-mcp 0.1.1 → 0.1.3
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 +2 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -0
- package/dist/index.js +20 -14
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -25,13 +25,14 @@ Running from source instead of npm: `"command": "node", "args": ["/path/to/whaml
|
|
|
25
25
|
|
|
26
26
|
### Environment
|
|
27
27
|
|
|
28
|
-
- `WHAMLINK_API_KEY` (
|
|
28
|
+
- `WHAMLINK_API_KEY` (optional) — your whamlink API key. If unset, the server still starts; the agent can `register` to create an account in-session (save the returned key here to persist).
|
|
29
29
|
- `WHAMLINK_BASE_URL` (optional) — defaults to `https://whamlink.com`.
|
|
30
30
|
|
|
31
31
|
## Tools
|
|
32
32
|
|
|
33
33
|
| Tool | What it does |
|
|
34
34
|
|------|--------------|
|
|
35
|
+
| `register` | Create an account on the user's behalf (consent + real email). Lets a new user go zero-to-published without setting a key first. |
|
|
35
36
|
| `publish_link` | Publish HTML / Markdown / PDF / image / text → a permanent URL. Public by default; set `visibility` to `private` / `password` / `email` to gate it. |
|
|
36
37
|
| `list_links` | List your published links (id, slug, mode, visibility, URL). |
|
|
37
38
|
| `set_link_access` | Change a link's visibility, password, shared-email list, `allowNetwork`, or title. |
|
package/dist/client.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface AccessArgs {
|
|
|
26
26
|
allowNetwork?: boolean;
|
|
27
27
|
title?: string;
|
|
28
28
|
}
|
|
29
|
+
export declare function registerAccount(cfg: WhamlinkConfig, args: {
|
|
30
|
+
email: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}): Promise<any>;
|
|
29
33
|
export declare const publishLink: (cfg: WhamlinkConfig, args: PublishArgs) => Promise<any>;
|
|
30
34
|
export declare const listLinks: (cfg: WhamlinkConfig) => Promise<any>;
|
|
31
35
|
export declare const deleteLink: (cfg: WhamlinkConfig, id: string) => Promise<any>;
|
package/dist/client.js
CHANGED
|
@@ -26,6 +26,18 @@ async function call(cfg, method, path, body) {
|
|
|
26
26
|
}
|
|
27
27
|
return json;
|
|
28
28
|
}
|
|
29
|
+
// Register a new account. No auth (there's no key yet) — returns { user, apiKey, _setup }.
|
|
30
|
+
export async function registerAccount(cfg, args) {
|
|
31
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
32
|
+
const res = await f(`${cfg.baseUrl}/v1/auth/register`, {
|
|
33
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(args),
|
|
34
|
+
});
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
const json = text ? JSON.parse(text) : undefined;
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
throw new WhamlinkError(json?.error?.code ?? "error", json?.error?.message ?? `Register failed (${res.status})`, res.status);
|
|
39
|
+
return json;
|
|
40
|
+
}
|
|
29
41
|
export const publishLink = (cfg, args) => call(cfg, "POST", "/v1/publish", args);
|
|
30
42
|
export const listLinks = (cfg) => call(cfg, "GET", "/v1/docs");
|
|
31
43
|
export const deleteLink = (cfg, id) => call(cfg, "DELETE", `/v1/docs/${encodeURIComponent(id)}`);
|
package/dist/index.js
CHANGED
|
@@ -2,28 +2,34 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { WhamlinkError, publishLink, listLinks, deleteLink, setLinkAccess, replaceLinkContent, } from "./client.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
process.exit(1);
|
|
10
|
-
}
|
|
11
|
-
const cfg = { baseUrl: process.env.WHAMLINK_BASE_URL ?? "https://whamlink.com", apiKey };
|
|
5
|
+
import { WhamlinkError, registerAccount, publishLink, listLinks, deleteLink, setLinkAccess, replaceLinkContent, } from "./client.js";
|
|
6
|
+
// The key is mutable: it can come from the env, or be set in-session by the `register` tool, so a
|
|
7
|
+
// brand-new user can go from zero to published in one conversation. cfg.apiKey is read at call time.
|
|
8
|
+
const cfg = { baseUrl: process.env.WHAMLINK_BASE_URL ?? "https://whamlink.com", apiKey: process.env.WHAMLINK_API_KEY ?? "" };
|
|
12
9
|
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
10
|
+
const errText = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
13
11
|
async function guard(fn, render) {
|
|
14
12
|
try {
|
|
15
13
|
return ok(render(await fn()));
|
|
16
14
|
}
|
|
17
15
|
catch (e) {
|
|
18
16
|
const msg = e instanceof WhamlinkError ? `whamlink error (${e.status} ${e.code}): ${e.message}` : `whamlink request failed: ${e.message}`;
|
|
19
|
-
return
|
|
17
|
+
return errText(msg);
|
|
20
18
|
}
|
|
21
19
|
}
|
|
20
|
+
// Tools that act on an account need a key. Fail with a clear instruction rather than a raw 401.
|
|
21
|
+
const needKey = () => cfg.apiKey ? null : errText("No whamlink API key. Ask the user for consent and their real email, call `register` to create an account (or have them set WHAMLINK_API_KEY in the MCP config), then retry.");
|
|
22
22
|
const visibility = z.enum(["public", "private", "password", "email"]);
|
|
23
23
|
// Text-based modes only: MCP tool args are JSON, which can't carry binary. PDF/image files must
|
|
24
24
|
// go through the dashboard or the multipart API, so they're intentionally not offered here.
|
|
25
25
|
const mode = z.enum(["sandboxed_html", "sanitized_html", "markdown", "text"]);
|
|
26
|
-
const server = new McpServer({ name: "whamlink", version: "0.1.
|
|
26
|
+
const server = new McpServer({ name: "whamlink", version: "0.1.2" });
|
|
27
|
+
server.tool("register", "Create a whamlink account on the USER'S behalf. Only call this with the user's explicit consent and their REAL email — a verification link is sent there. Returns an API key used for the rest of this session; tell the user to set it as WHAMLINK_API_KEY in their MCP config to keep it across restarts.", { email: z.string().email().describe("the user's real email"), name: z.string() }, (a) => guard(async () => {
|
|
28
|
+
const r = await registerAccount(cfg, a);
|
|
29
|
+
if (r?.apiKey)
|
|
30
|
+
cfg.apiKey = r.apiKey; // use it for the rest of this session
|
|
31
|
+
return r;
|
|
32
|
+
}, (r) => `Account created for ${r.user?.email ?? "the user"} — you can publish now.\nAPI key (save as WHAMLINK_API_KEY to persist): ${r.apiKey}\nA verification link was emailed; clicking it unlocks full quota (3 links allowed before verifying).`));
|
|
27
33
|
server.tool("publish_link", "Publish text-based content (HTML, Markdown, or plain text) to a permanent whamlink URL. Links are public (unlisted) by default; set visibility to private/password/email to gate them. (PDF/image files aren't supported over MCP — use the whamlink dashboard or multipart API for those.) Never publish secrets or private data.", {
|
|
28
34
|
mode: mode.describe("sandboxed_html (runs JS, isolated origin), sanitized_html (scripts stripped), markdown, or text"),
|
|
29
35
|
content: z.string().describe("The text content to publish (UTF-8)."),
|
|
@@ -32,10 +38,10 @@ server.tool("publish_link", "Publish text-based content (HTML, Markdown, or plai
|
|
|
32
38
|
visibility: visibility.optional().describe("Default public. private = owner only; password = also pass `password`; email = also pass `shareEmails`"),
|
|
33
39
|
password: z.string().min(6).optional(),
|
|
34
40
|
shareEmails: z.array(z.string().email()).optional(),
|
|
35
|
-
}, (a) => guard(() => publishLink(cfg, a), (r) => `Published: ${r.url}\n(id: ${r.id}, slug: ${r.slug})`));
|
|
36
|
-
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." :
|
|
41
|
+
}, (a) => needKey() ?? guard(() => publishLink(cfg, a), (r) => `Published: ${r.url}\n(id: ${r.id}, slug: ${r.slug})`));
|
|
42
|
+
server.tool("list_links", "List the links you've published (id, slug, title, mode, visibility, size, URL).", {}, () => needKey() ?? guard(() => listLinks(cfg), (r) => !r.docs?.length ? "No links yet." :
|
|
37
43
|
r.docs.map((d) => `• ${d.title || d.slug} — ${cfg.baseUrl}/${d.slug} [${d.mode}, ${d.visibility ?? "public"}] (id: ${d.id})`).join("\n")));
|
|
38
|
-
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}.`));
|
|
44
|
+
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) => needKey() ?? guard(() => deleteLink(cfg, a.id), () => `Deleted ${a.id}.`));
|
|
39
45
|
server.tool("set_link_access", "Change a link's access: visibility (public/private/password/email), password, shared email list, allowNetwork, or title.", {
|
|
40
46
|
id: z.string(),
|
|
41
47
|
visibility: visibility.optional(),
|
|
@@ -43,7 +49,7 @@ server.tool("set_link_access", "Change a link's access: visibility (public/priva
|
|
|
43
49
|
shareEmails: z.array(z.string().email()).optional(),
|
|
44
50
|
allowNetwork: z.boolean().optional(),
|
|
45
51
|
title: z.string().optional(),
|
|
46
|
-
}, ({ id, ...rest }) => guard(() => setLinkAccess(cfg, id, rest), () => `Updated access for ${id}.`));
|
|
47
|
-
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}).`));
|
|
52
|
+
}, ({ id, ...rest }) => needKey() ?? guard(() => setLinkAccess(cfg, id, rest), () => `Updated access for ${id}.`));
|
|
53
|
+
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 }) => needKey() ?? guard(() => replaceLinkContent(cfg, id, { mode, content }), (r) => `Replaced content for ${id} (${r.url}).`));
|
|
48
54
|
await server.connect(new StdioServerTransport());
|
|
49
55
|
process.stderr.write(`whamlink-mcp connected (base: ${cfg.baseUrl})\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whamlink-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for whamlink \u2014 publish a single file to a permanent, shareable link from any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"claude",
|
|
22
24
|
"mcp",
|
|
23
|
-
"
|
|
25
|
+
"mcp-server",
|
|
26
|
+
"model-context-protocol",
|
|
24
27
|
"publish",
|
|
25
|
-
"
|
|
28
|
+
"whamlink"
|
|
26
29
|
],
|
|
27
30
|
"license": "MIT",
|
|
28
31
|
"dependencies": {
|
|
@@ -33,5 +36,14 @@
|
|
|
33
36
|
"@types/node": "^26.0.1",
|
|
34
37
|
"typescript": "^5.5.0",
|
|
35
38
|
"vitest": "^2.1.0"
|
|
36
|
-
}
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://whamlink.com",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/billkinddev/whamlink-mcp.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/billkinddev/whamlink-mcp/issues"
|
|
47
|
+
},
|
|
48
|
+
"author": "whamlink"
|
|
37
49
|
}
|