@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 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`
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startThemeMcpServer } from '../src/server.mjs';
4
+
5
+ await startThemeMcpServer().catch((error) => {
6
+ console.error(error instanceof Error ? error.message : 'Failed to start Shoppex MCP server.');
7
+ process.exit(1);
8
+ });
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
+ });