@shoppexio/mcp-theme-server 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 +48 -0
- package/bin/shoppex-mcp-theme-server.mjs +8 -0
- package/package.json +43 -0
- package/src/server.mjs +296 -0
- package/src/server.test.mjs +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @shoppexio/mcp-theme-server
|
|
2
|
+
|
|
3
|
+
MCP server for Shoppex theme operations.
|
|
4
|
+
|
|
5
|
+
Use this package when you want an MCP-compatible client like Codex, Claude Desktop, or Cursor to work against hosted Shoppex themes.
|
|
6
|
+
|
|
7
|
+
## What It Exposes
|
|
8
|
+
|
|
9
|
+
The server exposes tools for:
|
|
10
|
+
- inspect and diff
|
|
11
|
+
- read and search
|
|
12
|
+
- apply changes
|
|
13
|
+
- create sections and pages
|
|
14
|
+
- validate, preview, publish, and rollback
|
|
15
|
+
|
|
16
|
+
## Runtime Config
|
|
17
|
+
|
|
18
|
+
Set these environment variables before starting the server:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export SHOPPEX_API_KEY=shx_your_key
|
|
22
|
+
export SHOPPEX_API_URL=https://api.shoppex.io
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Start
|
|
26
|
+
|
|
27
|
+
Install and run from npm:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx -y @shoppexio/mcp-theme-server
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or, if you are working from this repo:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun packages/mcp-theme-server/bin/shoppex-mcp-theme-server.mjs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Auth
|
|
40
|
+
|
|
41
|
+
Recommended scopes:
|
|
42
|
+
- `themes.read`
|
|
43
|
+
- `themes.write`
|
|
44
|
+
|
|
45
|
+
## Related Docs
|
|
46
|
+
|
|
47
|
+
- AI Workflows: `https://docs.shoppex.io/themes/ai-workflows`
|
|
48
|
+
- Theme Control Plane: `https://docs.shoppex.io/themes/theme-control-plane`
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shoppexio/mcp-theme-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shoppex MCP server for theme control plane operations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ShoppexIO/shoppex.git",
|
|
9
|
+
"directory": "packages/mcp-theme-server"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.shoppex.io/themes/ai-workflows",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ShoppexIO/shoppex/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"shoppex",
|
|
20
|
+
"mcp",
|
|
21
|
+
"theme",
|
|
22
|
+
"server",
|
|
23
|
+
"control-plane"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"shoppex-mcp-theme-server": "./bin/shoppex-mcp-theme-server.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
41
|
+
"@shoppexio/theme-control-client": "0.1.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import {
|
|
5
|
+
ShoppexThemeControlApiError,
|
|
6
|
+
ShoppexThemeControlClient,
|
|
7
|
+
resolveEnvThemeControlConfig,
|
|
8
|
+
} from '@shoppexio/theme-control-client';
|
|
9
|
+
|
|
10
|
+
function jsonContent(value) {
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: 'text',
|
|
15
|
+
text: JSON.stringify(value, null, 2),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
structuredContent: value,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createThemeToolCatalog() {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'theme.inspect',
|
|
26
|
+
description: 'Inspect a Shoppex theme and return editable areas, sections, settings, design tokens, and constraints.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
theme_id: z.string().min(1),
|
|
29
|
+
},
|
|
30
|
+
execute: ({ theme_id }, client) => client.inspectTheme(theme_id),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'theme.diff',
|
|
34
|
+
description: 'Show changes between the current draft and the latest published theme snapshot.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
theme_id: z.string().min(1),
|
|
37
|
+
},
|
|
38
|
+
execute: ({ theme_id }, client) => client.diffTheme(theme_id),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'theme.read_file',
|
|
42
|
+
description: 'Read a single source file from a Shoppex theme.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
theme_id: z.string().min(1),
|
|
45
|
+
file_path: z.string().min(1),
|
|
46
|
+
},
|
|
47
|
+
execute: ({ theme_id, file_path }, client) => client.readThemeFile(theme_id, file_path),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'theme.search_files',
|
|
51
|
+
description: 'Search theme files by path or content.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
theme_id: z.string().min(1),
|
|
54
|
+
query: z.string().min(1),
|
|
55
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
56
|
+
},
|
|
57
|
+
execute: ({ theme_id, query, limit }, client) => client.searchThemeFiles(theme_id, query, limit),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'theme.apply',
|
|
61
|
+
description: 'Apply one or more text-file changes to a theme draft with server-side verification.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
theme_id: z.string().min(1),
|
|
64
|
+
changes: z.array(z.object({
|
|
65
|
+
path: z.string().min(1),
|
|
66
|
+
content: z.string(),
|
|
67
|
+
})).min(1).max(50),
|
|
68
|
+
run_typecheck: z.boolean().optional(),
|
|
69
|
+
verification_strategy: z.enum(['quick', 'full']).optional(),
|
|
70
|
+
},
|
|
71
|
+
execute: ({ theme_id, changes, run_typecheck, verification_strategy }, client) => client.applyThemeChanges(theme_id, {
|
|
72
|
+
changes,
|
|
73
|
+
...(run_typecheck !== undefined ? { runTypecheck: run_typecheck } : {}),
|
|
74
|
+
...(verification_strategy ? { verificationStrategy: verification_strategy } : {}),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'theme.accept',
|
|
79
|
+
description: 'Accept an already generated draft patch from a dashboard-agent run.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
theme_id: z.string().min(1),
|
|
82
|
+
run_id: z.string().uuid(),
|
|
83
|
+
assistant_message_id: z.string().uuid(),
|
|
84
|
+
variant_id: z.string().min(1).optional(),
|
|
85
|
+
},
|
|
86
|
+
execute: ({ theme_id, run_id, assistant_message_id, variant_id }, client) => client.acceptThemeDraft(theme_id, {
|
|
87
|
+
runId: run_id,
|
|
88
|
+
assistantMessageId: assistant_message_id,
|
|
89
|
+
...(variant_id ? { variantId: variant_id } : {}),
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'theme.create',
|
|
94
|
+
description: 'Create a new theme scaffold from a Shoppex base theme.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
base: z.enum(['default', 'classic', 'nebula', 'pulse']),
|
|
97
|
+
name: z.string().min(1).optional(),
|
|
98
|
+
set_as_active: z.boolean().optional(),
|
|
99
|
+
},
|
|
100
|
+
execute: ({ base, name, set_as_active }, client) => client.createTheme({
|
|
101
|
+
base,
|
|
102
|
+
...(name ? { name } : {}),
|
|
103
|
+
...(set_as_active !== undefined ? { setAsActive: set_as_active } : {}),
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'theme.create_section',
|
|
108
|
+
description: 'Scaffold a new theme section and wire it into theme.config.ts.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
theme_id: z.string().min(1),
|
|
111
|
+
name: z.string().min(1),
|
|
112
|
+
description: z.string().optional(),
|
|
113
|
+
section_key: z.string().optional(),
|
|
114
|
+
},
|
|
115
|
+
execute: ({ theme_id, name, description, section_key }, client) => client.createThemeSection(theme_id, {
|
|
116
|
+
name,
|
|
117
|
+
...(description ? { description } : {}),
|
|
118
|
+
...(section_key ? { sectionKey: section_key } : {}),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'theme.create_page',
|
|
123
|
+
description: 'Scaffold a new static page and wire its route into src/App.tsx.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
theme_id: z.string().min(1),
|
|
126
|
+
name: z.string().min(1),
|
|
127
|
+
description: z.string().optional(),
|
|
128
|
+
route_path: z.string().optional(),
|
|
129
|
+
},
|
|
130
|
+
execute: ({ theme_id, name, description, route_path }, client) => client.createThemePage(theme_id, {
|
|
131
|
+
name,
|
|
132
|
+
...(description ? { description } : {}),
|
|
133
|
+
...(route_path ? { routePath: route_path } : {}),
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'theme.update_config',
|
|
138
|
+
description: 'Extend theme.config.ts with a new settings field.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
theme_id: z.string().min(1),
|
|
141
|
+
group: z.string().min(1),
|
|
142
|
+
key: z.string().min(1),
|
|
143
|
+
setting: z.record(z.string(), z.any()),
|
|
144
|
+
},
|
|
145
|
+
execute: ({ theme_id, group, key, setting }, client) => client.updateThemeConfig(theme_id, {
|
|
146
|
+
group,
|
|
147
|
+
key,
|
|
148
|
+
setting,
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'theme.preview',
|
|
153
|
+
description: 'Start or reuse a live preview session for the current theme draft.',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
theme_id: z.string().min(1),
|
|
156
|
+
},
|
|
157
|
+
execute: ({ theme_id }, client) => client.startThemePreview(theme_id),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'theme.stop_preview',
|
|
161
|
+
description: 'Stop a running live preview session.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
theme_id: z.string().min(1),
|
|
164
|
+
session_id: z.string().min(1),
|
|
165
|
+
},
|
|
166
|
+
execute: ({ theme_id, session_id }, client) => client.stopThemePreview(theme_id, session_id),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'theme.publish',
|
|
170
|
+
description: 'Queue a publish/deploy for the current accepted theme draft.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
theme_id: z.string().min(1),
|
|
173
|
+
},
|
|
174
|
+
execute: ({ theme_id }, client) => client.publishTheme(theme_id),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'theme.publish_status',
|
|
178
|
+
description: 'Read the current publish job status.',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
theme_id: z.string().min(1),
|
|
181
|
+
job_id: z.string().uuid(),
|
|
182
|
+
},
|
|
183
|
+
execute: ({ theme_id, job_id }, client) => client.getThemePublishStatus(theme_id, job_id),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'theme.validate',
|
|
187
|
+
description: 'Run a validation-only build and optional typecheck without publishing.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
theme_id: z.string().min(1),
|
|
190
|
+
include_typecheck: z.boolean().optional(),
|
|
191
|
+
},
|
|
192
|
+
execute: ({ theme_id, include_typecheck }, client) => client.validateTheme(theme_id, {
|
|
193
|
+
...(include_typecheck !== undefined ? { includeTypecheck: include_typecheck } : {}),
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'theme.settings_get',
|
|
198
|
+
description: 'Read the current builder settings values for a theme.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
theme_id: z.string().min(1),
|
|
201
|
+
},
|
|
202
|
+
execute: ({ theme_id }, client) => client.getThemeSettings(theme_id),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'theme.settings_update',
|
|
206
|
+
description: 'Update theme builder settings without editing source files directly.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
theme_id: z.string().min(1),
|
|
209
|
+
builder_settings: z.record(z.string(), z.any()),
|
|
210
|
+
},
|
|
211
|
+
execute: ({ theme_id, builder_settings }, client) => client.updateThemeSettings(theme_id, {
|
|
212
|
+
builder_settings,
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'theme.backups',
|
|
217
|
+
description: 'List available theme backups for rollback.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
theme_id: z.string().min(1),
|
|
220
|
+
},
|
|
221
|
+
execute: ({ theme_id }, client) => client.listThemeBackups(theme_id),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'theme.rollback',
|
|
225
|
+
description: 'Rollback a theme to a previous backup snapshot.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
theme_id: z.string().min(1),
|
|
228
|
+
backup_id: z.string().uuid(),
|
|
229
|
+
},
|
|
230
|
+
execute: ({ theme_id, backup_id }, client) => client.rollbackTheme(theme_id, backup_id),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'theme.latest_run',
|
|
234
|
+
description: 'Read the latest dashboard-agent run metadata for a theme.',
|
|
235
|
+
inputSchema: {
|
|
236
|
+
theme_id: z.string().min(1),
|
|
237
|
+
},
|
|
238
|
+
execute: ({ theme_id }, client) => client.getLatestThemeRun(theme_id),
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function executeThemeTool(toolName, args, client) {
|
|
244
|
+
const tool = createThemeToolCatalog().find((entry) => entry.name === toolName);
|
|
245
|
+
if (!tool) {
|
|
246
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return tool.execute(args, client);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function createThemeMcpServer(client = new ShoppexThemeControlClient(resolveEnvThemeControlConfig())) {
|
|
253
|
+
const server = new McpServer({
|
|
254
|
+
name: 'shoppex-themes',
|
|
255
|
+
version: '0.1.0',
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
for (const tool of createThemeToolCatalog()) {
|
|
259
|
+
server.registerTool(tool.name, {
|
|
260
|
+
description: tool.description,
|
|
261
|
+
inputSchema: tool.inputSchema,
|
|
262
|
+
}, async (args) => {
|
|
263
|
+
try {
|
|
264
|
+
const result = await tool.execute(args, client);
|
|
265
|
+
return jsonContent(result);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof ShoppexThemeControlApiError) {
|
|
268
|
+
return {
|
|
269
|
+
...jsonContent({
|
|
270
|
+
error: error.message,
|
|
271
|
+
status: error.status,
|
|
272
|
+
payload: error.payload,
|
|
273
|
+
}),
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
...jsonContent({
|
|
280
|
+
error: error instanceof Error ? error.message : 'Unknown MCP server error.',
|
|
281
|
+
}),
|
|
282
|
+
isError: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return server;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function startThemeMcpServer() {
|
|
292
|
+
const server = createThemeMcpServer();
|
|
293
|
+
const transport = new StdioServerTransport();
|
|
294
|
+
await server.connect(transport);
|
|
295
|
+
return server;
|
|
296
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createThemeToolCatalog, executeThemeTool } from './server.mjs';
|
|
3
|
+
|
|
4
|
+
describe('shoppex theme mcp server', () => {
|
|
5
|
+
test('registers the expected theme tools', () => {
|
|
6
|
+
const tools = createThemeToolCatalog().map((tool) => tool.name);
|
|
7
|
+
|
|
8
|
+
expect(tools).toContain('theme.inspect');
|
|
9
|
+
expect(tools).toContain('theme.apply');
|
|
10
|
+
expect(tools).toContain('theme.create');
|
|
11
|
+
expect(tools).toContain('theme.settings_update');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('maps theme.apply to the control plane client', async () => {
|
|
15
|
+
const client = {
|
|
16
|
+
applyThemeChanges: async (themeId, body) => ({ themeId, body }),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = await executeThemeTool('theme.apply', {
|
|
20
|
+
theme_id: 'theme_1',
|
|
21
|
+
changes: [
|
|
22
|
+
{
|
|
23
|
+
path: 'src/pages/Home.tsx',
|
|
24
|
+
content: 'export default function Home() { return <div>Updated</div>; }',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
run_typecheck: true,
|
|
28
|
+
verification_strategy: 'full',
|
|
29
|
+
}, client);
|
|
30
|
+
|
|
31
|
+
expect(result).toEqual({
|
|
32
|
+
themeId: 'theme_1',
|
|
33
|
+
body: {
|
|
34
|
+
changes: [
|
|
35
|
+
{
|
|
36
|
+
path: 'src/pages/Home.tsx',
|
|
37
|
+
content: 'export default function Home() { return <div>Updated</div>; }',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
runTypecheck: true,
|
|
41
|
+
verificationStrategy: 'full',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|