@werewolf-solutions/dev 0.0.0-stage → 0.1.1
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 +123 -2
- package/bin/werewolf-dev.js +36 -0
- package/package.json +26 -4
- package/src/agents.js +228 -0
- package/src/api.js +43 -0
- package/src/cli.js +81 -0
- package/src/contract.js +38 -0
- package/src/draft.js +80 -0
- package/src/mcpRunner.js +251 -0
- package/src/runner.js +168 -0
- package/werewolf-mcp.mjs +236 -0
package/werewolf-mcp.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Werewolf Solutions MCP server. Two kinds of key, two tool sets:
|
|
4
|
+
*
|
|
5
|
+
* - WEREWOLF_DEV_TOKEN, a developer's runner token: the website runner tools
|
|
6
|
+
* (src/mcpRunner.js). Your harness builds the drafts for the businesses assigned to you.
|
|
7
|
+
* - WEREWOLF_API_KEY, a business secret key: the site-builder tools below. The client LLM
|
|
8
|
+
* generates the website files itself and saves them as revisions; publishing swaps the
|
|
9
|
+
* placeholder tokens for a real publishable key server-side.
|
|
10
|
+
*
|
|
11
|
+
* WEREWOLF_API_URL is the site or its API, e.g. https://staging.werewolf.solutions
|
|
12
|
+
* (default https://werewolf.solutions). `werewolf-dev mcp --api <site>` sets it too.
|
|
13
|
+
*
|
|
14
|
+
* Register with Claude Code:
|
|
15
|
+
* claude mcp add werewolf --env WEREWOLF_DEV_TOKEN=ws_sk_... \
|
|
16
|
+
* -- npx -y @werewolf-solutions/dev mcp --api https://staging.werewolf.solutions
|
|
17
|
+
*/
|
|
18
|
+
import { createRequire } from 'module';
|
|
19
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const { apiBase } = require('./src/api.js');
|
|
25
|
+
const { startRunnerMcp } = require('./src/mcpRunner.js');
|
|
26
|
+
|
|
27
|
+
const API_URL = apiBase(process.env.WEREWOLF_API_URL || 'https://werewolf.solutions');
|
|
28
|
+
const DEV_TOKEN = process.env.WEREWOLF_DEV_TOKEN;
|
|
29
|
+
const PSM_READONLY = process.env.WEREWOLF_MCP_PROFILE === 'psm-readonly';
|
|
30
|
+
|
|
31
|
+
// A runner token serves the runner tools and stops here. Any other key in WEREWOLF_DEV_TOKEN
|
|
32
|
+
// (the server refuses it at the runner check-in) falls through to the business tools.
|
|
33
|
+
let runnerMode = false;
|
|
34
|
+
if (DEV_TOKEN) {
|
|
35
|
+
try {
|
|
36
|
+
runnerMode = await startRunnerMcp({ api: API_URL, token: DEV_TOKEN, McpServer, StdioServerTransport, z });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`[werewolf-dev mcp] could not sign in to ${API_URL}: ${error.response?.data?.message || error.message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
if (!runnerMode) console.error('[werewolf-dev mcp] WEREWOLF_DEV_TOKEN is not a website runner token; serving the business tools with it');
|
|
42
|
+
}
|
|
43
|
+
const API_KEY = process.env.WEREWOLF_API_KEY || DEV_TOKEN;
|
|
44
|
+
|
|
45
|
+
if (!runnerMode && (!API_KEY || !API_KEY.startsWith('ws_sk_'))) {
|
|
46
|
+
console.error('Set WEREWOLF_DEV_TOKEN to your runner token, or WEREWOLF_API_KEY to a business secret key (ws_sk_...)');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const api = async (method, path, body) => {
|
|
51
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
52
|
+
method,
|
|
53
|
+
headers: {
|
|
54
|
+
'X-API-Key': API_KEY,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
const data = await res.json().catch(() => ({}));
|
|
60
|
+
if (!res.ok || data.success === false) {
|
|
61
|
+
throw new Error(data.message || `${method} ${path} failed (${res.status})`);
|
|
62
|
+
}
|
|
63
|
+
return data.data ?? data;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The key is business-scoped; resolve its business once.
|
|
67
|
+
let businessCache = null;
|
|
68
|
+
const getBusiness = async () => {
|
|
69
|
+
if (!businessCache) {
|
|
70
|
+
const data = await api('GET', '/businesses/client');
|
|
71
|
+
businessCache = data.business;
|
|
72
|
+
}
|
|
73
|
+
return businessCache;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const text = (value) => ({
|
|
77
|
+
content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const server = new McpServer({ name: 'werewolf', version: '1.0.0' });
|
|
81
|
+
|
|
82
|
+
server.tool(
|
|
83
|
+
'get_psm_service_status',
|
|
84
|
+
'Read the paid PSM cloud service readiness for the business behind this key: Stripe Connect onboarding, recurring price configuration, object-storage readiness, and aggregate non-PII usage. Use this to validate the integration; it never changes billing or customer data.',
|
|
85
|
+
{},
|
|
86
|
+
async () => text(await api('GET', '/apps/psm/service-status'))
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!PSM_READONLY) {
|
|
90
|
+
server.tool(
|
|
91
|
+
'get_business',
|
|
92
|
+
'Get the business this API key belongs to: name, description, branding, services, contact details, brand profile. Use this as the authoritative content source when generating a website.',
|
|
93
|
+
{},
|
|
94
|
+
async () => text(await getBusiness())
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
server.tool(
|
|
99
|
+
'list_projects',
|
|
100
|
+
'List the website-builder projects for this business, including status and published URL.',
|
|
101
|
+
{},
|
|
102
|
+
async () => {
|
|
103
|
+
const business = await getBusiness();
|
|
104
|
+
const data = await api('GET', `/businesses/${business._id}/builder/projects`);
|
|
105
|
+
return text(
|
|
106
|
+
data.projects.map((p) => ({
|
|
107
|
+
id: p._id,
|
|
108
|
+
name: p.name,
|
|
109
|
+
status: p.status,
|
|
110
|
+
publishedUrl: p.publishedUrl,
|
|
111
|
+
sourceUrl: p.sourceUrl,
|
|
112
|
+
updatedAt: p.updatedAt,
|
|
113
|
+
}))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.tool(
|
|
119
|
+
'create_project',
|
|
120
|
+
'Create a new website project for the business. Optionally pass sourceUrl (an existing website to import content from) and a prompt. The platform generates a first revision server-side; you can replace it with save_revision.',
|
|
121
|
+
{
|
|
122
|
+
name: z.string().optional().describe('Project name'),
|
|
123
|
+
prompt: z.string().optional().describe('Style/content instructions'),
|
|
124
|
+
sourceUrl: z.string().optional().describe("URL of the business's existing website to import content from"),
|
|
125
|
+
},
|
|
126
|
+
async ({ name, prompt, sourceUrl }) => {
|
|
127
|
+
const business = await getBusiness();
|
|
128
|
+
const data = await api('POST', `/businesses/${business._id}/builder/projects`, {
|
|
129
|
+
name,
|
|
130
|
+
prompt,
|
|
131
|
+
sourceUrl,
|
|
132
|
+
});
|
|
133
|
+
return text({
|
|
134
|
+
projectId: data.project._id,
|
|
135
|
+
status: data.project.status,
|
|
136
|
+
importedContent: data.project.importedContent || null,
|
|
137
|
+
firstRevisionProvider: data.revision?.provider,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
'get_project',
|
|
144
|
+
'Get a project with its current files, revision history, and run log. Read the current files before revising them.',
|
|
145
|
+
{ projectId: z.string() },
|
|
146
|
+
async ({ projectId }) => {
|
|
147
|
+
const business = await getBusiness();
|
|
148
|
+
const data = await api('GET', `/businesses/${business._id}/builder/projects/${projectId}`);
|
|
149
|
+
const active = data.project.activeRevision;
|
|
150
|
+
return text({
|
|
151
|
+
project: {
|
|
152
|
+
id: data.project._id,
|
|
153
|
+
name: data.project.name,
|
|
154
|
+
status: data.project.status,
|
|
155
|
+
publishedUrl: data.project.publishedUrl,
|
|
156
|
+
sourceUrl: data.project.sourceUrl,
|
|
157
|
+
importedContent: data.project.importedContent || null,
|
|
158
|
+
},
|
|
159
|
+
activeRevision: active
|
|
160
|
+
? { id: active._id, provider: active.provider, prompt: active.prompt, files: active.files }
|
|
161
|
+
: null,
|
|
162
|
+
revisions: (data.revisions || []).map((r) => ({
|
|
163
|
+
id: r._id,
|
|
164
|
+
provider: r.provider,
|
|
165
|
+
status: r.status,
|
|
166
|
+
prompt: r.prompt,
|
|
167
|
+
createdAt: r.createdAt,
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
server.tool(
|
|
174
|
+
'save_revision',
|
|
175
|
+
[
|
|
176
|
+
'Save website files you generated as a new revision (becomes the active draft).',
|
|
177
|
+
'Rules for the files you generate:',
|
|
178
|
+
'- files must include index.html; static HTML/CSS only, CSS inlined in <style>; max 20 files / 5MB total.',
|
|
179
|
+
'- The contact section must contain <div id="werewolf-contact"></div> followed by:',
|
|
180
|
+
' <script src="__WEREWOLF_PLATFORM_URL__/sdk/v1/widget.js" data-key="__WEREWOLF_PUBLISHABLE_KEY__" data-api="__WEREWOLF_PLATFORM_URL__" data-target="#werewolf-contact"></script>',
|
|
181
|
+
'- Keep the __WEREWOLF_PUBLISHABLE_KEY__ and __WEREWOLF_PLATFORM_URL__ placeholders verbatim; publishing fills them in.',
|
|
182
|
+
'- Use only real business data from get_business / importedContent; never fabricate testimonials or credentials.',
|
|
183
|
+
].join('\n'),
|
|
184
|
+
{
|
|
185
|
+
projectId: z.string(),
|
|
186
|
+
prompt: z.string().optional().describe('What this revision changes (for the audit log)'),
|
|
187
|
+
files: z
|
|
188
|
+
.array(z.object({ path: z.string(), content: z.string() }))
|
|
189
|
+
.describe('The complete site files, e.g. [{ path: "index.html", content: "<!doctype html>..." }]'),
|
|
190
|
+
},
|
|
191
|
+
async ({ projectId, prompt, files }) => {
|
|
192
|
+
const business = await getBusiness();
|
|
193
|
+
const data = await api('POST', `/businesses/${business._id}/builder/projects/${projectId}/revisions`, {
|
|
194
|
+
prompt,
|
|
195
|
+
files,
|
|
196
|
+
provider: 'claude-code-mcp',
|
|
197
|
+
});
|
|
198
|
+
return text({ revisionId: data.revision._id, status: data.revision.status });
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
server.tool(
|
|
203
|
+
'generate_revision',
|
|
204
|
+
"Ask the PLATFORM to generate a revision server-side (template or configured AI provider) instead of generating files yourself. Usually you should generate the files and use save_revision; this is the fallback.",
|
|
205
|
+
{ projectId: z.string(), prompt: z.string() },
|
|
206
|
+
async ({ projectId, prompt }) => {
|
|
207
|
+
const business = await getBusiness();
|
|
208
|
+
const data = await api('POST', `/businesses/${business._id}/builder/projects/${projectId}/revisions`, {
|
|
209
|
+
prompt,
|
|
210
|
+
});
|
|
211
|
+
return text({ revisionId: data.revision._id, provider: data.revision.provider });
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
server.tool(
|
|
216
|
+
'publish_project',
|
|
217
|
+
'Publish the active revision as the live static site. Pass domains for every host the site will be served from (e.g. handymanservices.werewolf.solutions) so the embedded contact-widget key is domain-locked correctly.',
|
|
218
|
+
{
|
|
219
|
+
projectId: z.string(),
|
|
220
|
+
domains: z.array(z.string()).optional().describe('Extra hostnames the site will be served from'),
|
|
221
|
+
},
|
|
222
|
+
async ({ projectId, domains }) => {
|
|
223
|
+
const business = await getBusiness();
|
|
224
|
+
const data = await api('POST', `/businesses/${business._id}/builder/projects/${projectId}/publish`, {
|
|
225
|
+
domains,
|
|
226
|
+
});
|
|
227
|
+
return text({
|
|
228
|
+
publishedUrl: data.publishedUrl,
|
|
229
|
+
keyPreview: data.apiKey?.keyPreview,
|
|
230
|
+
allowedDomains: data.apiKey?.allowedDomains,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
if (!runnerMode) await server.connect(new StdioServerTransport());
|