@limeade-labs/sparkui 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/.env.example +9 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/SKILL.md +242 -0
- package/bin/deploy +23 -0
- package/bin/sparkui.js +390 -0
- package/docs/README.md +51 -0
- package/docs/api-reference.md +428 -0
- package/docs/chatgpt-setup.md +206 -0
- package/docs/components.md +432 -0
- package/docs/getting-started.md +179 -0
- package/docs/mcp-setup.md +195 -0
- package/docs/openclaw-setup.md +177 -0
- package/docs/templates.md +289 -0
- package/lib/components.js +474 -0
- package/lib/store.js +193 -0
- package/lib/templates.js +48 -0
- package/lib/ws-client.js +197 -0
- package/mcp-server/README.md +189 -0
- package/mcp-server/index.js +174 -0
- package/mcp-server/package.json +15 -0
- package/package.json +52 -0
- package/server.js +620 -0
- package/templates/base.js +82 -0
- package/templates/checkout.js +271 -0
- package/templates/feedback-form.js +140 -0
- package/templates/macro-tracker.js +205 -0
- package/templates/workout-timer.js +510 -0
- package/templates/ws-test.js +136 -0
package/lib/templates.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
5
|
+
|
|
6
|
+
// Registry of known templates
|
|
7
|
+
const registry = {
|
|
8
|
+
'macro-tracker': require(path.join(TEMPLATES_DIR, 'macro-tracker')),
|
|
9
|
+
'ws-test': require(path.join(TEMPLATES_DIR, 'ws-test')),
|
|
10
|
+
'feedback-form': require(path.join(TEMPLATES_DIR, 'feedback-form')),
|
|
11
|
+
'checkout': require(path.join(TEMPLATES_DIR, 'checkout')),
|
|
12
|
+
'workout-timer': require(path.join(TEMPLATES_DIR, 'workout-timer')),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render a named template with data.
|
|
17
|
+
* @param {string} name - Template name (e.g. "macro-tracker")
|
|
18
|
+
* @param {object} data - Template data
|
|
19
|
+
* @returns {string} Rendered HTML
|
|
20
|
+
* @throws {Error} if template not found
|
|
21
|
+
*/
|
|
22
|
+
function render(name, data) {
|
|
23
|
+
const templateFn = registry[name];
|
|
24
|
+
if (!templateFn) {
|
|
25
|
+
const available = Object.keys(registry).join(', ');
|
|
26
|
+
throw new Error(`Unknown template "${name}". Available: ${available}`);
|
|
27
|
+
}
|
|
28
|
+
return templateFn(data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a template exists.
|
|
33
|
+
* @param {string} name
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function has(name) {
|
|
37
|
+
return name in registry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List available template names.
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
function list() {
|
|
45
|
+
return Object.keys(registry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { render, has, list };
|
package/lib/ws-client.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client-side WebSocket code for SparkUI pages.
|
|
5
|
+
* Exported as a string so it can be inlined into HTML templates.
|
|
6
|
+
* No external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const WS_CLIENT_JS = `
|
|
10
|
+
(function() {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
var pageId = window.__SPARKUI_PAGE_ID__;
|
|
14
|
+
if (!pageId) return;
|
|
15
|
+
|
|
16
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
17
|
+
var wsUrl = proto + '//' + location.host + '/ws?page=' + encodeURIComponent(pageId);
|
|
18
|
+
|
|
19
|
+
var ws = null;
|
|
20
|
+
var reconnectDelay = 1000;
|
|
21
|
+
var maxReconnectDelay = 30000;
|
|
22
|
+
var heartbeatTimer = null;
|
|
23
|
+
var connected = false;
|
|
24
|
+
var messageHandlers = [];
|
|
25
|
+
var pendingQueue = [];
|
|
26
|
+
var completionData = {};
|
|
27
|
+
|
|
28
|
+
// ── Connection Management ──
|
|
29
|
+
|
|
30
|
+
function connect() {
|
|
31
|
+
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return;
|
|
32
|
+
|
|
33
|
+
try { ws = new WebSocket(wsUrl); } catch(e) { scheduleReconnect(); return; }
|
|
34
|
+
|
|
35
|
+
ws.onopen = function() {
|
|
36
|
+
connected = true;
|
|
37
|
+
reconnectDelay = 1000;
|
|
38
|
+
startHeartbeat();
|
|
39
|
+
dispatchStatus('connected');
|
|
40
|
+
|
|
41
|
+
// Flush pending messages
|
|
42
|
+
while (pendingQueue.length > 0) {
|
|
43
|
+
var msg = pendingQueue.shift();
|
|
44
|
+
try { ws.send(msg); } catch(e) {}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
ws.onmessage = function(e) {
|
|
49
|
+
try {
|
|
50
|
+
var msg = JSON.parse(e.data);
|
|
51
|
+
} catch(err) { return; }
|
|
52
|
+
|
|
53
|
+
// Handle built-in message types
|
|
54
|
+
if (msg.type === 'update') {
|
|
55
|
+
location.reload();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (msg.type === 'destroy') {
|
|
60
|
+
stopHeartbeat();
|
|
61
|
+
document.body.innerHTML = '<div style="background:#111;color:#888;font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><div style="text-align:center"><h1 style="font-size:3rem;margin-bottom:8px">⚡</h1><p>This page has expired or been removed.</p><p style="color:#555;font-size:0.85rem">SparkUI pages are ephemeral by design.</p></div></div>';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (msg.type === 'pong') {
|
|
66
|
+
// Server heartbeat response, connection is alive
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Dispatch to registered handlers
|
|
71
|
+
for (var i = 0; i < messageHandlers.length; i++) {
|
|
72
|
+
try { messageHandlers[i](msg); } catch(err) { console.error('[sparkui] handler error:', err); }
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
ws.onclose = function() {
|
|
77
|
+
connected = false;
|
|
78
|
+
stopHeartbeat();
|
|
79
|
+
dispatchStatus('disconnected');
|
|
80
|
+
scheduleReconnect();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
ws.onerror = function() {
|
|
84
|
+
// onclose will fire after this
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scheduleReconnect() {
|
|
89
|
+
setTimeout(function() {
|
|
90
|
+
connect();
|
|
91
|
+
}, reconnectDelay);
|
|
92
|
+
reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Heartbeat ──
|
|
96
|
+
|
|
97
|
+
function startHeartbeat() {
|
|
98
|
+
stopHeartbeat();
|
|
99
|
+
heartbeatTimer = setInterval(function() {
|
|
100
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
101
|
+
try { ws.send(JSON.stringify({ type: 'heartbeat' })); } catch(e) {}
|
|
102
|
+
}
|
|
103
|
+
}, 25000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stopHeartbeat() {
|
|
107
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Send Messages ──
|
|
111
|
+
|
|
112
|
+
function send(type, data) {
|
|
113
|
+
var msg = JSON.stringify({ type: type, pageId: pageId, data: data || {}, timestamp: Date.now() });
|
|
114
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
115
|
+
try { ws.send(msg); } catch(e) { pendingQueue.push(msg); }
|
|
116
|
+
} else {
|
|
117
|
+
pendingQueue.push(msg);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Event Handlers ──
|
|
122
|
+
|
|
123
|
+
function onMessage(handler) {
|
|
124
|
+
if (typeof handler === 'function') messageHandlers.push(handler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function offMessage(handler) {
|
|
128
|
+
messageHandlers = messageHandlers.filter(function(h) { return h !== handler; });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Status Dispatch ──
|
|
132
|
+
|
|
133
|
+
function dispatchStatus(status) {
|
|
134
|
+
var event;
|
|
135
|
+
try {
|
|
136
|
+
event = new CustomEvent('sparkui:status', { detail: { status: status, pageId: pageId } });
|
|
137
|
+
} catch(e) {
|
|
138
|
+
event = document.createEvent('CustomEvent');
|
|
139
|
+
event.initCustomEvent('sparkui:status', true, true, { status: status, pageId: pageId });
|
|
140
|
+
}
|
|
141
|
+
document.dispatchEvent(event);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Completion on Unload ──
|
|
145
|
+
|
|
146
|
+
function setCompletionData(data) {
|
|
147
|
+
completionData = data || {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sendCompletion(data) {
|
|
151
|
+
var payload = data || completionData;
|
|
152
|
+
send('completion', payload);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Send completion on page unload
|
|
156
|
+
window.addEventListener('beforeunload', function() {
|
|
157
|
+
if (ws && ws.readyState === WebSocket.OPEN && Object.keys(completionData).length > 0) {
|
|
158
|
+
// Use sendBeacon-style: just try to send, no guarantee
|
|
159
|
+
try { ws.send(JSON.stringify({ type: 'completion', pageId: pageId, data: completionData, timestamp: Date.now() })); } catch(e) {}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Public API ──
|
|
164
|
+
|
|
165
|
+
window.sparkui = {
|
|
166
|
+
send: send,
|
|
167
|
+
onMessage: onMessage,
|
|
168
|
+
offMessage: offMessage,
|
|
169
|
+
setCompletionData: setCompletionData,
|
|
170
|
+
sendCompletion: sendCompletion,
|
|
171
|
+
get connected() { return connected; },
|
|
172
|
+
get pageId() { return pageId; }
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Auto-connect
|
|
176
|
+
connect();
|
|
177
|
+
})();
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the client JS with pageId injected.
|
|
182
|
+
* @param {string} pageId
|
|
183
|
+
* @returns {string} Script tag content
|
|
184
|
+
*/
|
|
185
|
+
function getClientScript(pageId) {
|
|
186
|
+
return WS_CLIENT_JS.replace('window.__SPARKUI_PAGE_ID__', JSON.stringify(pageId));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the raw client JS string (for manual injection).
|
|
191
|
+
* @returns {string}
|
|
192
|
+
*/
|
|
193
|
+
function getRawClientJS() {
|
|
194
|
+
return WS_CLIENT_JS;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { getClientScript, getRawClientJS };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# SparkUI MCP Server
|
|
2
|
+
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that exposes SparkUI as tools for AI clients like Claude Desktop, Cursor, Windsurf, and Cline.
|
|
4
|
+
|
|
5
|
+
Any MCP-compatible client can create ephemeral web pages on the fly — checkout flows, workout timers, dashboards, forms — through natural language.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd mcp-server
|
|
11
|
+
npm install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
The server uses two environment variables:
|
|
17
|
+
|
|
18
|
+
| Variable | Description | Default |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `SPARKUI_URL` | Base URL of the SparkUI server | `http://localhost:3457` |
|
|
21
|
+
| `SPARKUI_TOKEN` | Push API authentication token | *(required)* |
|
|
22
|
+
|
|
23
|
+
## Claude Desktop Setup
|
|
24
|
+
|
|
25
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"sparkui": {
|
|
31
|
+
"command": "node",
|
|
32
|
+
"args": ["/absolute/path/to/sparkui/mcp-server/index.js"],
|
|
33
|
+
"env": {
|
|
34
|
+
"SPARKUI_URL": "http://localhost:3457",
|
|
35
|
+
"SPARKUI_TOKEN": "your-push-token"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Cursor / Windsurf Setup
|
|
43
|
+
|
|
44
|
+
Add to `.cursor/mcp.json` or `.windsurf/mcp.json` in your project root:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"sparkui": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["/absolute/path/to/sparkui/mcp-server/index.js"],
|
|
52
|
+
"env": {
|
|
53
|
+
"SPARKUI_URL": "http://localhost:3457",
|
|
54
|
+
"SPARKUI_TOKEN": "your-push-token"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Available Tools
|
|
62
|
+
|
|
63
|
+
### `sparkui_list_templates`
|
|
64
|
+
|
|
65
|
+
List available page templates.
|
|
66
|
+
|
|
67
|
+
**Parameters:** None
|
|
68
|
+
|
|
69
|
+
**Returns:**
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"templates": ["macro-tracker", "checkout", "workout-timer", "feedback-form", "ws-test"]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `sparkui_list_components`
|
|
77
|
+
|
|
78
|
+
List available components for composing pages.
|
|
79
|
+
|
|
80
|
+
**Parameters:** None
|
|
81
|
+
|
|
82
|
+
**Returns:**
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"components": ["header", "button", "timer", "checklist", "progress", ...]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `sparkui_push`
|
|
90
|
+
|
|
91
|
+
Create a page from a template.
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
| Name | Type | Required | Description |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `template` | string | ✅ | Template name (e.g. `"checkout"`) |
|
|
97
|
+
| `data` | object | ✅ | Template data |
|
|
98
|
+
| `ttl` | number | | Time-to-live in seconds (default: 3600) |
|
|
99
|
+
| `og` | object | | Open Graph overrides `{title, description, image}` |
|
|
100
|
+
|
|
101
|
+
**Example:**
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"template": "checkout",
|
|
105
|
+
"data": {
|
|
106
|
+
"title": "Your Order",
|
|
107
|
+
"items": [{"name": "Widget", "price": 9.99, "qty": 2}],
|
|
108
|
+
"total": 19.98
|
|
109
|
+
},
|
|
110
|
+
"ttl": 7200
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Returns:**
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"id": "abc-123-...",
|
|
118
|
+
"url": "/s/abc-123-...",
|
|
119
|
+
"fullUrl": "http://localhost:3457/s/abc-123-..."
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `sparkui_compose`
|
|
124
|
+
|
|
125
|
+
Compose a page from individual components.
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
| Name | Type | Required | Description |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `title` | string | ✅ | Page title |
|
|
131
|
+
| `sections` | array | ✅ | Array of `{type, config}` sections |
|
|
132
|
+
| `ttl` | number | | Time-to-live in seconds |
|
|
133
|
+
|
|
134
|
+
**Example:**
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"title": "Quick Poll",
|
|
138
|
+
"sections": [
|
|
139
|
+
{"type": "header", "config": {"text": "What should we build?"}},
|
|
140
|
+
{"type": "checklist", "config": {"items": ["Feature A", "Feature B", "Feature C"]}}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Returns:**
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"id": "def-456-...",
|
|
149
|
+
"url": "/s/def-456-...",
|
|
150
|
+
"fullUrl": "http://localhost:3457/s/def-456-..."
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `sparkui_page_status`
|
|
155
|
+
|
|
156
|
+
Check if a page exists and whether it's still active.
|
|
157
|
+
|
|
158
|
+
**Parameters:**
|
|
159
|
+
| Name | Type | Required | Description |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `id` | string | ✅ | Page UUID |
|
|
162
|
+
|
|
163
|
+
**Returns:**
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"exists": true,
|
|
167
|
+
"status": "active",
|
|
168
|
+
"url": "/s/abc-123-...",
|
|
169
|
+
"fullUrl": "http://localhost:3457/s/abc-123-..."
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## How It Works
|
|
174
|
+
|
|
175
|
+
The MCP server communicates with the SparkUI HTTP API over HTTP. It does **not** import SparkUI internals — it's a standalone client that calls `POST /api/push`, `POST /api/compose`, and `GET /` on the SparkUI server.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Claude Desktop / Cursor / Windsurf
|
|
179
|
+
↓ (MCP stdio)
|
|
180
|
+
SparkUI MCP Server
|
|
181
|
+
↓ (HTTP)
|
|
182
|
+
SparkUI Server (localhost:3457)
|
|
183
|
+
↓
|
|
184
|
+
Ephemeral web pages
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
6
|
+
const { z } = require('zod/v4');
|
|
7
|
+
|
|
8
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const SPARKUI_URL = (process.env.SPARKUI_URL || 'http://localhost:3457').replace(/\/+$/, '');
|
|
11
|
+
const SPARKUI_TOKEN = process.env.SPARKUI_TOKEN || '';
|
|
12
|
+
|
|
13
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
async function sparkuiRequest(method, path, body) {
|
|
16
|
+
const url = `${SPARKUI_URL}${path}`;
|
|
17
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
18
|
+
if (SPARKUI_TOKEN) {
|
|
19
|
+
headers['Authorization'] = `Bearer ${SPARKUI_TOKEN}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const options = { method, headers };
|
|
23
|
+
if (body) options.body = JSON.stringify(body);
|
|
24
|
+
|
|
25
|
+
const res = await fetch(url, options);
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(data.error || `SparkUI API error: ${res.status}`);
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const server = new McpServer({
|
|
37
|
+
name: 'sparkui',
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Tool: sparkui_list_templates
|
|
42
|
+
server.tool(
|
|
43
|
+
'sparkui_list_templates',
|
|
44
|
+
'List available SparkUI page templates. Returns template names that can be used with sparkui_push.',
|
|
45
|
+
{},
|
|
46
|
+
async () => {
|
|
47
|
+
const data = await sparkuiRequest('GET', '/');
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: JSON.stringify({ templates: data.templates }, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Tool: sparkui_list_components
|
|
55
|
+
server.tool(
|
|
56
|
+
'sparkui_list_components',
|
|
57
|
+
'List available SparkUI components for composing pages with sparkui_compose.',
|
|
58
|
+
{},
|
|
59
|
+
async () => {
|
|
60
|
+
const components = [
|
|
61
|
+
'header', 'button', 'timer', 'checklist', 'progress',
|
|
62
|
+
'text', 'image', 'divider', 'card', 'input',
|
|
63
|
+
'select', 'table', 'chart', 'code', 'form'
|
|
64
|
+
];
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify({ components }, null, 2) }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Tool: sparkui_push
|
|
72
|
+
server.tool(
|
|
73
|
+
'sparkui_push',
|
|
74
|
+
'Push a page from a SparkUI template. Returns the page ID and URL.',
|
|
75
|
+
{
|
|
76
|
+
template: z.string().describe('Template name (e.g. "checkout", "macro-tracker", "workout-timer")'),
|
|
77
|
+
data: z.record(z.string(), z.any()).describe('Template data object'),
|
|
78
|
+
ttl: z.optional(z.number().describe('Time-to-live in seconds (default 3600)')),
|
|
79
|
+
og: z.optional(z.object({
|
|
80
|
+
title: z.optional(z.string()),
|
|
81
|
+
description: z.optional(z.string()),
|
|
82
|
+
image: z.optional(z.string()),
|
|
83
|
+
}).describe('Open Graph metadata overrides')),
|
|
84
|
+
},
|
|
85
|
+
async ({ template, data, ttl, og }) => {
|
|
86
|
+
const body = { template, data };
|
|
87
|
+
if (ttl !== undefined) body.ttl = ttl;
|
|
88
|
+
if (og) body.og = og;
|
|
89
|
+
|
|
90
|
+
const result = await sparkuiRequest('POST', '/api/push', body);
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Tool: sparkui_compose
|
|
98
|
+
server.tool(
|
|
99
|
+
'sparkui_compose',
|
|
100
|
+
'Compose a page from SparkUI components. Returns the page ID and URL.',
|
|
101
|
+
{
|
|
102
|
+
title: z.string().describe('Page title'),
|
|
103
|
+
sections: z.array(z.object({
|
|
104
|
+
type: z.string().describe('Component type (e.g. "header", "button", "timer", "checklist")'),
|
|
105
|
+
config: z.optional(z.record(z.string(), z.any()).describe('Component configuration')),
|
|
106
|
+
})).describe('Array of sections to compose'),
|
|
107
|
+
ttl: z.optional(z.number().describe('Time-to-live in seconds')),
|
|
108
|
+
},
|
|
109
|
+
async ({ title, sections, ttl }) => {
|
|
110
|
+
const body = { title, sections };
|
|
111
|
+
if (ttl !== undefined) body.ttl = ttl;
|
|
112
|
+
|
|
113
|
+
const result = await sparkuiRequest('POST', '/api/compose', body);
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Tool: sparkui_page_status
|
|
121
|
+
server.tool(
|
|
122
|
+
'sparkui_page_status',
|
|
123
|
+
'Check if a SparkUI page exists and get its status.',
|
|
124
|
+
{
|
|
125
|
+
id: z.string().describe('Page UUID'),
|
|
126
|
+
},
|
|
127
|
+
async ({ id }) => {
|
|
128
|
+
try {
|
|
129
|
+
const url = `${SPARKUI_URL}/s/${id}`;
|
|
130
|
+
const res = await fetch(url);
|
|
131
|
+
|
|
132
|
+
if (res.status === 410) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: 'text', text: JSON.stringify({ exists: false, status: 'expired' }, null, 2) }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (res.status === 404) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: 'text', text: JSON.stringify({ exists: false, status: 'not_found' }, null, 2) }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content: [{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: JSON.stringify({
|
|
148
|
+
exists: true,
|
|
149
|
+
status: 'active',
|
|
150
|
+
url: `/s/${id}`,
|
|
151
|
+
fullUrl: `${SPARKUI_URL}/s/${id}`,
|
|
152
|
+
}, null, 2),
|
|
153
|
+
}],
|
|
154
|
+
};
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: 'text', text: JSON.stringify({ exists: false, error: err.message }, null, 2) }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ── Start ────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const transport = new StdioServerTransport();
|
|
167
|
+
await server.connect(transport);
|
|
168
|
+
console.error('SparkUI MCP server running on stdio');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch((err) => {
|
|
172
|
+
console.error('Fatal:', err);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sparkui/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing SparkUI as tools for Claude Desktop, Cursor, Windsurf, and other MCP clients",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sparkui-mcp": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@limeade-labs/sparkui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ephemeral, purpose-built web UIs generated on demand from chat conversations",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sparkui": "./bin/sparkui.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js",
|
|
11
|
+
"dev": "node server.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"lib/",
|
|
16
|
+
"templates/",
|
|
17
|
+
"server.js",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CONTRIBUTING.md",
|
|
21
|
+
".env.example",
|
|
22
|
+
"docs/",
|
|
23
|
+
"SKILL.md",
|
|
24
|
+
"mcp-server/index.js",
|
|
25
|
+
"mcp-server/package.json",
|
|
26
|
+
"mcp-server/README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sparkui",
|
|
33
|
+
"ephemeral-ui",
|
|
34
|
+
"ai-agent",
|
|
35
|
+
"web-ui",
|
|
36
|
+
"mcp",
|
|
37
|
+
"websocket",
|
|
38
|
+
"templates"
|
|
39
|
+
],
|
|
40
|
+
"homepage": "https://sparkui.dev",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/limeade-labs/sparkui.git"
|
|
44
|
+
},
|
|
45
|
+
"author": "Limeade Labs",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"express": "^4.21.0",
|
|
49
|
+
"uuid": "^11.1.0",
|
|
50
|
+
"ws": "^8.18.0"
|
|
51
|
+
}
|
|
52
|
+
}
|