divmagic-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DivMagic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # DivMagic MCP
2
+
3
+ Copy any component from a live web page as clean, self-contained HTML or JSX — from inside your agent.
4
+
5
+ DivMagic opens the page in a real browser and converts what it *renders*. That is the difference from fetching HTML: the output carries the computed styles, the resolved layout, and the markup the page actually produced, not the template it shipped.
6
+
7
+ ```
8
+ You: copy the pricing table from stripe.com/pricing as JSX with tailwind
9
+ Agent: → copy_component_from_url
10
+ ← <section className="flex flex-col gap-8 …">…</section>
11
+ ```
12
+
13
+ ## Get a key
14
+
15
+ 1. Sign in at [divmagic.com/dashboard](https://divmagic.com/dashboard)
16
+ 2. Open **MCP** and choose **Create key**
17
+ 3. Copy it straight away
18
+
19
+ Keys look like `dvmcp_…`, and are shown once. Losing a key means issuing a new one, which immediately revokes the old.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npx divmagic-mcp
25
+ ```
26
+
27
+ It asks for your key, checks it against the server before touching anything, shows which agents it found, and writes only the ones you pick.
28
+
29
+ ```
30
+ Checking the key… works. (copy_component_from_url, get_component_result)
31
+
32
+ Found 3 on this machine. Which should get DivMagic?
33
+ 1. Claude Code
34
+ 2. Cursor
35
+ 3. VS Code
36
+
37
+ Which? (numbers separated by commas, or "all") all
38
+ ```
39
+
40
+ Twenty-two agents are supported. Run `npx divmagic-mcp --help` for the list.
41
+
42
+ ```bash
43
+ npx divmagic-mcp --key dvmcp_YOUR_KEY -a cursor -a claude-code -y
44
+ ```
45
+
46
+ Restart any agent it changed.
47
+
48
+ <details>
49
+ <summary>Or configure it by hand</summary>
50
+
51
+ **Claude Code**
52
+
53
+ ```bash
54
+ claude mcp add --transport http divmagic https://api.divmagic.com/v1/mcp \
55
+ --header "Authorization: Bearer dvmcp_YOUR_KEY"
56
+ ```
57
+
58
+ **Cursor** (`~/.cursor/mcp.json`), **VS Code** (`.vscode/mcp.json`), **Windsurf** (`~/.codeium/windsurf/mcp_config.json`)
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "divmagic": {
64
+ "url": "https://api.divmagic.com/v1/mcp",
65
+ "headers": { "Authorization": "Bearer dvmcp_YOUR_KEY" }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Claude Desktop**
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "divmagic": {
77
+ "command": "npx",
78
+ "args": [
79
+ "mcp-remote",
80
+ "https://api.divmagic.com/v1/mcp",
81
+ "--header", "Authorization: Bearer dvmcp_YOUR_KEY"
82
+ ]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ </details>
89
+
90
+ ## Tools
91
+
92
+ ### `copy_component_from_url`
93
+
94
+ | Parameter | Type | Default | |
95
+ |---|---|---|---|
96
+ | `url` | string | — | The page to copy. A bare host works: `stripe.com/pricing`. |
97
+ | `componentFormat` | `html` \| `jsx` | `html` | Output language. |
98
+ | `styleFormat` | `inline` \| `tailwind` \| `local` \| `external` | `tailwind` | How styling is emitted. |
99
+ | `refresh` | boolean | `false` | Render the page again instead of reusing a recent copy. Costs a credit. |
100
+
101
+ A copy usually takes 10–30 seconds. If it has not finished in about twenty, you get a job id — `get_component_result` collects it.
102
+
103
+ ### `get_component_result`
104
+
105
+ | Parameter | Type | |
106
+ |---|---|---|
107
+ | `jobId` | string | The id `copy_component_from_url` gave you. |
108
+ | `offset` | integer | Byte offset. Omit for the whole result. |
109
+ | `maxBytes` | integer | Bytes per piece, up to 256 KB. |
110
+
111
+ Polling costs nothing.
112
+
113
+ ## How it behaves
114
+
115
+ **Large results.** Over 256 KB is not returned inline. You get the size, a download link valid for an hour, and the option to read by `offset`.
116
+
117
+ **Caching.** A rendered copy is reused for 24 hours. Pass `refresh: true` when a page has changed.
118
+
119
+ **Credits.** One credit per copy you receive. A failed copy costs nothing. Polling costs nothing.
120
+
121
+ **Limits.** A page gets 60 seconds to render. Pages behind a login, or that block automated browsers, will not copy.
122
+
123
+ ## Troubleshooting
124
+
125
+ **`401` "Send your DivMagic MCP key"** — the `Authorization` header is not reaching us.
126
+
127
+ **`401` "This MCP key is not valid"** — the key was revoked. Create another in the dashboard.
128
+
129
+ **"Your MCP quota is exhausted"** — top up in the dashboard.
130
+
131
+ **"The attempt on this job stopped without finishing"** — call `copy_component_from_url` again with the same arguments.
132
+
133
+ ## Support
134
+
135
+ A page that copied badly: [open an issue](https://github.com/DivMagicCom/mcp/issues).
136
+
137
+ Account, billing and key questions: [divmagic.com](https://divmagic.com).
138
+
139
+ ## License
140
+
141
+ MIT — see [LICENSE](LICENSE). The DivMagic service is governed by its [terms](https://divmagic.com/terms).
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import { agents, detectGlobalAgents, getAgentTypes, upsertServer } from 'add-mcp';
3
+
4
+ import { verifyKey } from '../src/verify.js';
5
+ import { ask, askSecret, choose, close, bold, dim, green, red, yellow } from '../src/prompt.js';
6
+
7
+ const DEFAULT_ENDPOINT = 'https://api.divmagic.com/v1/mcp';
8
+ const DASHBOARD = 'https://divmagic.com/dashboard';
9
+ const SERVER_NAME = 'divmagic';
10
+
11
+ function parseArgs(argv) {
12
+ const args = { agents: null };
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const [flag, inline] = argv[i].split('=');
15
+ const value = () => inline ?? argv[++i];
16
+ if (flag === '--key') args.key = value();
17
+ else if (flag === '--endpoint') args.endpoint = value();
18
+ else if (flag === '--agent' || flag === '-a') (args.agents ||= []).push(value());
19
+ else if (flag === '--all') args.all = true;
20
+ else if (flag === '--yes' || flag === '-y') args.yes = true;
21
+ else if (flag === '--help' || flag === '-h') args.help = true;
22
+ }
23
+ return args;
24
+ }
25
+
26
+ function usage() {
27
+ console.log(`
28
+ ${bold('divmagic-mcp')} — install DivMagic into your coding agents
29
+
30
+ ${dim('$')} npx divmagic-mcp
31
+
32
+ Options
33
+ --key <key> Your DivMagic MCP key. Asked for if not given.
34
+ -a, --agent <id> Install only to this agent. Repeatable.
35
+ --all Install to every supported agent, not only those found.
36
+ -y, --yes Take the detected agents without asking.
37
+ --endpoint <url> Override the server address.
38
+ -h, --help This.
39
+
40
+ Supported agents (${getAgentTypes().length}):
41
+ ${getAgentTypes().join(', ')}
42
+
43
+ Your key comes from ${DASHBOARD}
44
+ `);
45
+ }
46
+
47
+ function label(agentType) {
48
+ return agents[agentType]?.displayName || agentType;
49
+ }
50
+
51
+ async function main() {
52
+ const args = parseArgs(process.argv.slice(2));
53
+ if (args.help) return usage();
54
+
55
+ const endpoint = args.endpoint || DEFAULT_ENDPOINT;
56
+
57
+ console.log(`\n${bold('DivMagic')} — copy any component from a live web page, from inside your agent.\n`);
58
+
59
+ let key = args.key;
60
+ if (!key) {
61
+ console.log(`Create a key at ${bold(DASHBOARD)} — it is shown once, so copy it before leaving the page.\n`);
62
+ key = await askSecret('Paste your key: ');
63
+ }
64
+ if (!key) {
65
+ console.log(red('\nNo key given. Nothing was changed.'));
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ process.stdout.write('\nChecking the key… ');
71
+ const check = await verifyKey(endpoint, key);
72
+ if (!check.ok) {
73
+ console.log(red('no.'));
74
+ console.log(`\n${check.reason}`);
75
+ console.log(dim('Nothing was changed.'));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ console.log(green('works.') + dim(` (${check.tools.join(', ')})`));
80
+
81
+ const detected = await detectGlobalAgents();
82
+ let chosen;
83
+
84
+ if (args.agents) {
85
+ const known = getAgentTypes();
86
+ const unknown = args.agents.filter((a) => !known.includes(a));
87
+ if (unknown.length > 0) {
88
+ console.log(red(`\nNot an agent this knows about: ${unknown.join(', ')}`));
89
+ console.log(dim(`Try one of: ${known.join(', ')}`));
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ chosen = args.agents;
94
+ } else if (args.all) {
95
+ chosen = getAgentTypes();
96
+ } else if (detected.length === 0) {
97
+ console.log(yellow('\nNo supported agent found on this machine.'));
98
+ console.log(dim('Install one, or name it directly with --agent. See --help for the list.'));
99
+ process.exitCode = 1;
100
+ return;
101
+ } else if (args.yes) {
102
+ chosen = detected;
103
+ } else {
104
+ const picked = await choose(
105
+ `Found ${detected.length} on this machine. Which should get DivMagic?`,
106
+ detected.map((type) => ({ id: type, label: label(type) })),
107
+ );
108
+ if (picked.length === 0) {
109
+ console.log(dim('\nNothing chosen. Nothing was changed.'));
110
+ return;
111
+ }
112
+ chosen = picked.map((p) => p.id);
113
+ }
114
+
115
+ console.log('');
116
+ const serverConfig = { type: 'http', url: endpoint, headers: { Authorization: `Bearer ${key}` } };
117
+ let failures = 0;
118
+
119
+ for (const agentType of chosen) {
120
+ const result = upsertServer(agentType, SERVER_NAME, serverConfig, { local: false });
121
+ if (result.success) {
122
+ console.log(` ${green('✓')} ${label(agentType)} ${dim(result.path)}`);
123
+ if (result.droppedFields?.length) {
124
+ console.log(` ${yellow('!')} ${dim(`this agent ignores: ${result.droppedFields.join(', ')}`)}`);
125
+ }
126
+ } else {
127
+ failures++;
128
+ console.log(` ${red('✗')} ${label(agentType)} — ${result.error || 'could not write its config'}`);
129
+ }
130
+ }
131
+
132
+ console.log(`\n${bold('Restart the agents you just changed')} — none of them reread their config while running.`);
133
+ console.log(`Then ask one to ${dim('"copy the pricing table from stripe.com/pricing as JSX"')}.\n`);
134
+ console.log(dim(`Server name: ${SERVER_NAME} Endpoint: ${endpoint}`));
135
+
136
+ if (failures > 0) process.exitCode = 1;
137
+ }
138
+
139
+ main()
140
+ .catch((error) => {
141
+ console.error(red(`\n${error.message}`));
142
+ process.exitCode = 1;
143
+ })
144
+ .finally(close);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "divmagic-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Install the DivMagic MCP server into the agents you already use",
5
+ "bin": {
6
+ "divmagic-mcp": "bin/divmagic-mcp.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "modelcontextprotocol",
20
+ "divmagic"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/DivMagicCom/mcp.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/DivMagicCom/mcp/issues"
29
+ },
30
+ "homepage": "https://divmagic.com",
31
+ "dependencies": {
32
+ "add-mcp": "^2.4.0"
33
+ }
34
+ }
package/src/prompt.js ADDED
@@ -0,0 +1,57 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+
4
+ const ESC = '\u001b[';
5
+ const wrap = (code, s) => `${ESC}${code}m${s}${ESC}0m`;
6
+
7
+ export const bold = (s) => wrap(1, s);
8
+ export const dim = (s) => wrap(2, s);
9
+ export const green = (s) => wrap(32, s);
10
+ export const red = (s) => wrap(31, s);
11
+ export const yellow = (s) => wrap(33, s);
12
+
13
+ let rl;
14
+ function io() {
15
+ if (!rl) rl = createInterface({ input: stdin, output: stdout });
16
+ return rl;
17
+ }
18
+
19
+ export function close() {
20
+ if (rl) rl.close();
21
+ rl = undefined;
22
+ }
23
+
24
+ export async function ask(question) {
25
+ const answer = await io().question(question);
26
+ return answer.trim();
27
+ }
28
+
29
+ export async function askSecret(question) {
30
+ const answer = await io().question(question);
31
+ stdout.write(`${ESC}1A${ESC}2K${question}${dim('(hidden)')}\n`);
32
+ return answer.trim();
33
+ }
34
+
35
+ export async function choose(question, options) {
36
+ console.log('\n' + question);
37
+ options.forEach((option, i) => {
38
+ const note = option.note ? ' ' + dim(option.note) : '';
39
+ console.log(` ${bold(String(i + 1))}. ${option.label}${note}`);
40
+ });
41
+
42
+ const raw = await ask(`\nWhich? ${dim('(numbers separated by commas, or "all")')} `);
43
+ if (!raw || raw.toLowerCase() === 'all') return options;
44
+
45
+ const picked = raw
46
+ .split(',')
47
+ .map((part) => parseInt(part.trim(), 10))
48
+ .filter((n) => n >= 1 && n <= options.length)
49
+ .map((n) => options[n - 1]);
50
+
51
+ return [...new Set(picked)];
52
+ }
53
+
54
+ export async function confirm(question) {
55
+ const answer = await ask(`${question} ${dim('(y/n)')} `);
56
+ return /^y(es)?$/i.test(answer);
57
+ }
package/src/verify.js ADDED
@@ -0,0 +1,57 @@
1
+ const LIST_TOOLS = {
2
+ jsonrpc: '2.0',
3
+ id: 1,
4
+ method: 'tools/list',
5
+ params: {},
6
+ };
7
+
8
+ function parseReply(body) {
9
+ const text = body.trim();
10
+ const payload = text.startsWith('event:') || text.startsWith('data:')
11
+ ? text
12
+ .split('\n')
13
+ .filter((line) => line.startsWith('data:'))
14
+ .map((line) => line.slice(5).trim())
15
+ .join('')
16
+ : text;
17
+ return JSON.parse(payload);
18
+ }
19
+
20
+ export async function verifyKey(endpoint, key) {
21
+ let response;
22
+ try {
23
+ response = await fetch(endpoint, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ Accept: 'application/json, text/event-stream',
28
+ Authorization: `Bearer ${key}`,
29
+ },
30
+ body: JSON.stringify(LIST_TOOLS),
31
+ signal: AbortSignal.timeout(20000),
32
+ });
33
+ } catch (error) {
34
+ return {
35
+ ok: false,
36
+ reason:
37
+ error.name === 'TimeoutError'
38
+ ? 'The server did not answer in time. Check your connection and try again.'
39
+ : `Could not reach ${endpoint}.`,
40
+ };
41
+ }
42
+
43
+ if (response.status === 401) {
44
+ return { ok: false, reason: 'That key was not accepted. Check it, or create a new one in the dashboard.' };
45
+ }
46
+ if (!response.ok) {
47
+ return { ok: false, reason: `The server answered ${response.status}.` };
48
+ }
49
+
50
+ try {
51
+ const message = parseReply(await response.text());
52
+ if (message.error) return { ok: false, reason: message.error.message || 'The server returned an error.' };
53
+ return { ok: true, tools: (message.result?.tools || []).map((tool) => tool.name) };
54
+ } catch {
55
+ return { ok: false, reason: 'The server’s reply could not be read.' };
56
+ }
57
+ }