@byfungsi/funforge 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 +273 -0
- package/dist/__tests__/api.test.d.ts +5 -0
- package/dist/__tests__/api.test.d.ts.map +1 -0
- package/dist/__tests__/api.test.js +177 -0
- package/dist/__tests__/config.test.d.ts +5 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +58 -0
- package/dist/__tests__/mcp.test.d.ts +7 -0
- package/dist/__tests__/mcp.test.d.ts.map +1 -0
- package/dist/__tests__/mcp.test.js +142 -0
- package/dist/__tests__/project-config.test.d.ts +5 -0
- package/dist/__tests__/project-config.test.d.ts.map +1 -0
- package/dist/__tests__/project-config.test.js +122 -0
- package/dist/__tests__/tarball.test.d.ts +5 -0
- package/dist/__tests__/tarball.test.d.ts.map +1 -0
- package/dist/__tests__/tarball.test.js +113 -0
- package/dist/api.d.ts +157 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +165 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +129 -0
- package/dist/commands/apps.d.ts +29 -0
- package/dist/commands/apps.d.ts.map +1 -0
- package/dist/commands/apps.js +151 -0
- package/dist/commands/auth.d.ts +27 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +127 -0
- package/dist/commands/config.d.ts +31 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +287 -0
- package/dist/commands/deploy.d.ts +24 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +196 -0
- package/dist/commands/domains.d.ts +35 -0
- package/dist/commands/domains.d.ts.map +1 -0
- package/dist/commands/domains.js +217 -0
- package/dist/commands/env.d.ts +26 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +183 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/credentials.d.ts +46 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +60 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +480 -0
- package/dist/project-config.d.ts +47 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +55 -0
- package/dist/tarball.d.ts +29 -0
- package/dist/tarball.d.ts.map +1 -0
- package/dist/tarball.js +148 -0
- package/package.json +45 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FunForge MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Model Context Protocol server for editor integration.
|
|
6
|
+
* Exposes FunForge CLI functionality as MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Usage in claude_desktop_config.json:
|
|
9
|
+
* {
|
|
10
|
+
* "mcpServers": {
|
|
11
|
+
* "funforge": {
|
|
12
|
+
* "command": "npx",
|
|
13
|
+
* "args": ["@byfungsi/funforge-mcp"]
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import { ApiError, apiRequest, createApp, createDeployment, getUploadUrl, listApps, uploadTarball, } from "./api.js";
|
|
22
|
+
import { getCredentials, isAuthenticated } from "./credentials.js";
|
|
23
|
+
import { getLinkedAppId, readConfig } from "./project-config.js";
|
|
24
|
+
import { createTarball, formatSize, readTarball } from "./tarball.js";
|
|
25
|
+
// ============================================
|
|
26
|
+
// TOOL DEFINITIONS
|
|
27
|
+
// ============================================
|
|
28
|
+
const TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: "funforge_whoami",
|
|
31
|
+
description: "Show the currently authenticated FunForge user. Returns email and user ID.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {},
|
|
35
|
+
required: [],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "funforge_apps_list",
|
|
40
|
+
description: "List all apps owned by the authenticated user. Returns app names, IDs, and slugs.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {},
|
|
44
|
+
required: [],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "funforge_apps_create",
|
|
49
|
+
description: "Create a new FunForge app.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
name: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Name of the app",
|
|
56
|
+
},
|
|
57
|
+
slug: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Custom slug/subdomain (optional, auto-generated if not provided)",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["name"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "funforge_status",
|
|
67
|
+
description: "Get the deployment status for the linked app in the current directory. Shows current and active deployments.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
directory: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Directory to check (defaults to current working directory)",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: [],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "funforge_deploy",
|
|
81
|
+
description: "Deploy the current directory to FunForge. Creates a tarball of project files, uploads to R2, and triggers deployment. The directory must be linked to an app via funforge.json.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
directory: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Directory to deploy (defaults to current working directory)",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "funforge_env_list",
|
|
95
|
+
description: "List environment variables for the linked app. Shows both app-level and user-level (inherited) env vars.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
directory: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Directory with funforge.json (defaults to cwd)",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: [],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "funforge_env_set",
|
|
109
|
+
description: "Set environment variables for the linked app. Changes take effect on next deployment.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
directory: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Directory with funforge.json (defaults to cwd)",
|
|
116
|
+
},
|
|
117
|
+
envVars: {
|
|
118
|
+
type: "object",
|
|
119
|
+
description: "Key-value pairs of environment variables to set",
|
|
120
|
+
additionalProperties: { type: "string" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["envVars"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "funforge_env_unset",
|
|
128
|
+
description: "Remove environment variables from the linked app.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
directory: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Directory with funforge.json (defaults to cwd)",
|
|
135
|
+
},
|
|
136
|
+
keys: {
|
|
137
|
+
type: "array",
|
|
138
|
+
items: { type: "string" },
|
|
139
|
+
description: "List of environment variable keys to remove",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["keys"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "funforge_domains_list",
|
|
147
|
+
description: "List custom domains configured for the linked app.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
directory: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Directory with funforge.json (defaults to cwd)",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: [],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "funforge_domains_add",
|
|
161
|
+
description: "Add a custom domain to the linked app. Returns DNS configuration instructions.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
directory: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Directory with funforge.json (defaults to cwd)",
|
|
168
|
+
},
|
|
169
|
+
domain: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Domain name to add (e.g., example.com)",
|
|
172
|
+
},
|
|
173
|
+
verificationMethod: {
|
|
174
|
+
type: "string",
|
|
175
|
+
enum: ["cname", "txt"],
|
|
176
|
+
description: "DNS verification method (default: cname)",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ["domain"],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
async function handleToolCall(name, args) {
|
|
184
|
+
try {
|
|
185
|
+
switch (name) {
|
|
186
|
+
case "funforge_whoami":
|
|
187
|
+
return await handleWhoami();
|
|
188
|
+
case "funforge_apps_list":
|
|
189
|
+
return await handleAppsList();
|
|
190
|
+
case "funforge_apps_create":
|
|
191
|
+
return await handleAppsCreate(args.name, args.slug);
|
|
192
|
+
case "funforge_status":
|
|
193
|
+
return await handleStatus(args.directory);
|
|
194
|
+
case "funforge_deploy":
|
|
195
|
+
return await handleDeploy(args.directory);
|
|
196
|
+
case "funforge_env_list":
|
|
197
|
+
return await handleEnvList(args.directory);
|
|
198
|
+
case "funforge_env_set":
|
|
199
|
+
return await handleEnvSet(args.directory, args.envVars);
|
|
200
|
+
case "funforge_env_unset":
|
|
201
|
+
return await handleEnvUnset(args.directory, args.keys);
|
|
202
|
+
case "funforge_domains_list":
|
|
203
|
+
return await handleDomainsList(args.directory);
|
|
204
|
+
case "funforge_domains_add":
|
|
205
|
+
return await handleDomainsAdd(args.directory, args.domain, args.verificationMethod);
|
|
206
|
+
default:
|
|
207
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return errorResult(formatError(error));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ============================================
|
|
215
|
+
// HANDLER IMPLEMENTATIONS
|
|
216
|
+
// ============================================
|
|
217
|
+
async function handleWhoami() {
|
|
218
|
+
if (!isAuthenticated()) {
|
|
219
|
+
return errorResult("Not authenticated. User needs to run `funforge login` in their terminal first.");
|
|
220
|
+
}
|
|
221
|
+
const creds = getCredentials();
|
|
222
|
+
return successResult({
|
|
223
|
+
authenticated: true,
|
|
224
|
+
email: creds.email,
|
|
225
|
+
userId: creds.userId,
|
|
226
|
+
authenticatedAt: creds.savedAt,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async function handleAppsList() {
|
|
230
|
+
requireAuth();
|
|
231
|
+
const { apps } = await listApps();
|
|
232
|
+
return successResult({
|
|
233
|
+
count: apps.length,
|
|
234
|
+
apps: apps.map((app) => ({
|
|
235
|
+
id: app.id,
|
|
236
|
+
name: app.name,
|
|
237
|
+
slug: app.slug,
|
|
238
|
+
sourceType: app.sourceType,
|
|
239
|
+
createdAt: app.createdAt,
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function handleAppsCreate(name, slug) {
|
|
244
|
+
requireAuth();
|
|
245
|
+
const { app } = await createApp({ name, slug });
|
|
246
|
+
return successResult({
|
|
247
|
+
message: `App "${app.name}" created successfully`,
|
|
248
|
+
app: {
|
|
249
|
+
id: app.id,
|
|
250
|
+
name: app.name,
|
|
251
|
+
slug: app.slug,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function handleStatus(directory) {
|
|
256
|
+
requireAuth();
|
|
257
|
+
const dir = directory || process.cwd();
|
|
258
|
+
const appId = await getLinkedAppId(dir);
|
|
259
|
+
if (!appId) {
|
|
260
|
+
return errorResult(`No app linked in ${dir}. Create funforge.json with an appId or run \`funforge link\` command.`);
|
|
261
|
+
}
|
|
262
|
+
const status = await apiRequest(`/api/cli/apps/${appId}/status`);
|
|
263
|
+
return successResult({
|
|
264
|
+
app: status.app,
|
|
265
|
+
currentDeployment: status.currentDeployment,
|
|
266
|
+
activeDeployment: status.activeDeployment,
|
|
267
|
+
url: status.app.subdomain
|
|
268
|
+
? `https://${status.app.subdomain}.funforge.app`
|
|
269
|
+
: null,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function handleDeploy(directory) {
|
|
273
|
+
requireAuth();
|
|
274
|
+
const dir = directory || process.cwd();
|
|
275
|
+
const config = await readConfig(dir);
|
|
276
|
+
if (!config?.appId) {
|
|
277
|
+
return errorResult(`No app linked in ${dir}. Create funforge.json with an appId or run \`funforge link\` command.`);
|
|
278
|
+
}
|
|
279
|
+
// 1. Create tarball
|
|
280
|
+
const tarballResult = await createTarball(dir);
|
|
281
|
+
// Check size limit (50MB)
|
|
282
|
+
const MAX_SIZE = 50 * 1024 * 1024;
|
|
283
|
+
if (tarballResult.size > MAX_SIZE) {
|
|
284
|
+
return errorResult(`Package too large: ${formatSize(tarballResult.size)} (max ${formatSize(MAX_SIZE)}). Add patterns to .funforgeignore to reduce size.`);
|
|
285
|
+
}
|
|
286
|
+
// 2. Get upload URL
|
|
287
|
+
const uploadInfo = await getUploadUrl(config.appId, {
|
|
288
|
+
filename: "context.tar.gz",
|
|
289
|
+
contentLength: tarballResult.size,
|
|
290
|
+
sha256: tarballResult.sha256,
|
|
291
|
+
});
|
|
292
|
+
// 3. Upload tarball
|
|
293
|
+
const tarballBuffer = await readTarball(tarballResult.path);
|
|
294
|
+
await uploadTarball(uploadInfo.uploadUrl, tarballBuffer);
|
|
295
|
+
// 4. Trigger deployment
|
|
296
|
+
const deployResult = await createDeployment(config.appId, {
|
|
297
|
+
sourceId: uploadInfo.sourceId,
|
|
298
|
+
});
|
|
299
|
+
return successResult({
|
|
300
|
+
message: "Deployment started successfully",
|
|
301
|
+
deployment: {
|
|
302
|
+
id: deployResult.deployment.id,
|
|
303
|
+
status: deployResult.deployment.status,
|
|
304
|
+
createdAt: deployResult.deployment.createdAt,
|
|
305
|
+
},
|
|
306
|
+
package: {
|
|
307
|
+
files: tarballResult.fileCount,
|
|
308
|
+
size: formatSize(tarballResult.size),
|
|
309
|
+
},
|
|
310
|
+
tip: "Use funforge_status to check deployment progress",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async function handleEnvList(directory) {
|
|
314
|
+
requireAuth();
|
|
315
|
+
const dir = directory || process.cwd();
|
|
316
|
+
const appId = await getLinkedAppId(dir);
|
|
317
|
+
if (!appId) {
|
|
318
|
+
return errorResult(`No app linked in ${dir}.`);
|
|
319
|
+
}
|
|
320
|
+
const { appEnvVars, userEnvVars } = await apiRequest(`/api/cli/apps/${appId}/env`);
|
|
321
|
+
return successResult({
|
|
322
|
+
appEnvVars,
|
|
323
|
+
userEnvVars,
|
|
324
|
+
note: "User env vars are inherited but can be overridden by app env vars",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function handleEnvSet(directory, envVars) {
|
|
328
|
+
requireAuth();
|
|
329
|
+
const dir = directory || process.cwd();
|
|
330
|
+
const appId = await getLinkedAppId(dir);
|
|
331
|
+
if (!appId) {
|
|
332
|
+
return errorResult(`No app linked in ${dir}.`);
|
|
333
|
+
}
|
|
334
|
+
await apiRequest(`/api/cli/apps/${appId}/env`, {
|
|
335
|
+
method: "PATCH",
|
|
336
|
+
body: { envVars },
|
|
337
|
+
});
|
|
338
|
+
return successResult({
|
|
339
|
+
message: `Set ${Object.keys(envVars).length} environment variable(s)`,
|
|
340
|
+
keys: Object.keys(envVars),
|
|
341
|
+
note: "Changes take effect on next deployment",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async function handleEnvUnset(directory, keys) {
|
|
345
|
+
requireAuth();
|
|
346
|
+
const dir = directory || process.cwd();
|
|
347
|
+
const appId = await getLinkedAppId(dir);
|
|
348
|
+
if (!appId) {
|
|
349
|
+
return errorResult(`No app linked in ${dir}.`);
|
|
350
|
+
}
|
|
351
|
+
const envVars = {};
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
envVars[key] = null;
|
|
354
|
+
}
|
|
355
|
+
await apiRequest(`/api/cli/apps/${appId}/env`, {
|
|
356
|
+
method: "PATCH",
|
|
357
|
+
body: { envVars },
|
|
358
|
+
});
|
|
359
|
+
return successResult({
|
|
360
|
+
message: `Removed ${keys.length} environment variable(s)`,
|
|
361
|
+
keys,
|
|
362
|
+
note: "Changes take effect on next deployment",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async function handleDomainsList(directory) {
|
|
366
|
+
requireAuth();
|
|
367
|
+
const dir = directory || process.cwd();
|
|
368
|
+
const appId = await getLinkedAppId(dir);
|
|
369
|
+
if (!appId) {
|
|
370
|
+
return errorResult(`No app linked in ${dir}.`);
|
|
371
|
+
}
|
|
372
|
+
const { domains } = await apiRequest(`/api/cli/apps/${appId}/domains`);
|
|
373
|
+
return successResult({
|
|
374
|
+
count: domains.length,
|
|
375
|
+
domains: domains.map((d) => ({
|
|
376
|
+
id: d.id,
|
|
377
|
+
domain: d.domain,
|
|
378
|
+
verified: d.verified,
|
|
379
|
+
sslStatus: d.sslStatus,
|
|
380
|
+
})),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
async function handleDomainsAdd(directory, domain, verificationMethod) {
|
|
384
|
+
requireAuth();
|
|
385
|
+
const dir = directory || process.cwd();
|
|
386
|
+
const appId = await getLinkedAppId(dir);
|
|
387
|
+
if (!appId) {
|
|
388
|
+
return errorResult(`No app linked in ${dir}.`);
|
|
389
|
+
}
|
|
390
|
+
// Normalize domain
|
|
391
|
+
const cleanDomain = domain
|
|
392
|
+
.replace(/^https?:\/\//, "")
|
|
393
|
+
.replace(/\/.*$/, "")
|
|
394
|
+
.toLowerCase();
|
|
395
|
+
const result = await apiRequest(`/api/cli/apps/${appId}/domains`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: {
|
|
398
|
+
domain: cleanDomain,
|
|
399
|
+
verificationMethod: verificationMethod || "cname",
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
return successResult({
|
|
403
|
+
message: `Domain ${cleanDomain} added`,
|
|
404
|
+
domain: result.domain,
|
|
405
|
+
dnsConfiguration: {
|
|
406
|
+
recordType: result.verification.type.toUpperCase(),
|
|
407
|
+
name: result.verification.name,
|
|
408
|
+
value: result.verification.value,
|
|
409
|
+
},
|
|
410
|
+
nextStep: "Add the DNS record above, then verify with funforge domains verify command",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// ============================================
|
|
414
|
+
// HELPERS
|
|
415
|
+
// ============================================
|
|
416
|
+
function requireAuth() {
|
|
417
|
+
if (!isAuthenticated()) {
|
|
418
|
+
throw new Error("Not authenticated. User needs to run `funforge login` in their terminal first.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function successResult(data) {
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: "text",
|
|
426
|
+
text: JSON.stringify(data, null, 2),
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function errorResult(message) {
|
|
432
|
+
return {
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "text",
|
|
436
|
+
text: JSON.stringify({ error: message }, null, 2),
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
isError: true,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function formatError(error) {
|
|
443
|
+
if (error instanceof ApiError) {
|
|
444
|
+
const body = error.body;
|
|
445
|
+
const message = body?.message || body?.error?.message || error.message;
|
|
446
|
+
return `API Error ${error.statusCode}: ${message}`;
|
|
447
|
+
}
|
|
448
|
+
return error instanceof Error ? error.message : String(error);
|
|
449
|
+
}
|
|
450
|
+
// ============================================
|
|
451
|
+
// SERVER SETUP
|
|
452
|
+
// ============================================
|
|
453
|
+
async function main() {
|
|
454
|
+
const server = new Server({
|
|
455
|
+
name: "funforge",
|
|
456
|
+
version: "0.1.0",
|
|
457
|
+
}, {
|
|
458
|
+
capabilities: {
|
|
459
|
+
tools: {},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
// Register tool listing handler
|
|
463
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
464
|
+
return { tools: TOOLS };
|
|
465
|
+
});
|
|
466
|
+
// Register tool call handler
|
|
467
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
468
|
+
const { name, arguments: args } = request.params;
|
|
469
|
+
return handleToolCall(name, args ?? {});
|
|
470
|
+
});
|
|
471
|
+
// Connect via stdio
|
|
472
|
+
const transport = new StdioServerTransport();
|
|
473
|
+
await server.connect(transport);
|
|
474
|
+
// Log to stderr (stdout is for MCP protocol)
|
|
475
|
+
console.error("FunForge MCP server started");
|
|
476
|
+
}
|
|
477
|
+
main().catch((error) => {
|
|
478
|
+
console.error("Failed to start MCP server:", error);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Configuration (funforge.json)
|
|
3
|
+
*
|
|
4
|
+
* Handles reading and writing funforge.json config files.
|
|
5
|
+
*/
|
|
6
|
+
export interface FunForgeConfig {
|
|
7
|
+
/** App ID this directory is linked to */
|
|
8
|
+
appId?: string;
|
|
9
|
+
/** App name (for display) */
|
|
10
|
+
appName?: string;
|
|
11
|
+
/** App slug (subdomain) */
|
|
12
|
+
appSlug?: string;
|
|
13
|
+
/** Build settings */
|
|
14
|
+
build?: {
|
|
15
|
+
/** Custom build command */
|
|
16
|
+
buildCommand?: string;
|
|
17
|
+
/** Custom install command */
|
|
18
|
+
installCommand?: string;
|
|
19
|
+
/** Custom start command */
|
|
20
|
+
startCommand?: string;
|
|
21
|
+
/** Node.js version */
|
|
22
|
+
nodeVersion?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Port the app listens on */
|
|
25
|
+
port?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if funforge.json exists in the directory
|
|
29
|
+
*/
|
|
30
|
+
export declare function configExists(dir?: string): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Read funforge.json from directory
|
|
33
|
+
*/
|
|
34
|
+
export declare function readConfig(dir?: string): Promise<FunForgeConfig | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Write funforge.json to directory
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeConfig(config: FunForgeConfig, dir?: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Update funforge.json (merge with existing)
|
|
41
|
+
*/
|
|
42
|
+
export declare function updateConfig(updates: Partial<FunForgeConfig>, dir?: string): Promise<FunForgeConfig>;
|
|
43
|
+
/**
|
|
44
|
+
* Get linked app ID from config
|
|
45
|
+
*/
|
|
46
|
+
export declare function getLinkedAppId(dir?: string): Promise<string | null>;
|
|
47
|
+
//# sourceMappingURL=project-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-config.d.ts","sourceRoot":"","sources":["../src/project-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,KAAK,CAAC,EAAE;QACN,2BAA2B;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,6BAA6B;QAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,2BAA2B;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,sBAAsB;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAID;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,GAAE,MAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAOtE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,EAChC,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,GAAE,MAAY,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Configuration (funforge.json)
|
|
3
|
+
*
|
|
4
|
+
* Handles reading and writing funforge.json config files.
|
|
5
|
+
*/
|
|
6
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
const CONFIG_FILENAME = "funforge.json";
|
|
9
|
+
/**
|
|
10
|
+
* Check if funforge.json exists in the directory
|
|
11
|
+
*/
|
|
12
|
+
export async function configExists(dir = ".") {
|
|
13
|
+
try {
|
|
14
|
+
await access(join(dir, CONFIG_FILENAME));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read funforge.json from directory
|
|
23
|
+
*/
|
|
24
|
+
export async function readConfig(dir = ".") {
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(join(dir, CONFIG_FILENAME), "utf-8");
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write funforge.json to directory
|
|
35
|
+
*/
|
|
36
|
+
export async function writeConfig(config, dir = ".") {
|
|
37
|
+
const content = JSON.stringify(config, null, 2) + "\n";
|
|
38
|
+
await writeFile(join(dir, CONFIG_FILENAME), content, "utf-8");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Update funforge.json (merge with existing)
|
|
42
|
+
*/
|
|
43
|
+
export async function updateConfig(updates, dir = ".") {
|
|
44
|
+
const existing = (await readConfig(dir)) ?? {};
|
|
45
|
+
const updated = { ...existing, ...updates };
|
|
46
|
+
await writeConfig(updated, dir);
|
|
47
|
+
return updated;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get linked app ID from config
|
|
51
|
+
*/
|
|
52
|
+
export async function getLinkedAppId(dir = ".") {
|
|
53
|
+
const config = await readConfig(dir);
|
|
54
|
+
return config?.appId ?? null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tarball Utilities
|
|
3
|
+
*
|
|
4
|
+
* Creates gzipped tarballs from project directories,
|
|
5
|
+
* respecting .gitignore and .funforgeignore patterns.
|
|
6
|
+
*/
|
|
7
|
+
export interface TarballResult {
|
|
8
|
+
/** Path to the created tarball */
|
|
9
|
+
path: string;
|
|
10
|
+
/** Size in bytes */
|
|
11
|
+
size: number;
|
|
12
|
+
/** SHA-256 hash */
|
|
13
|
+
sha256: string;
|
|
14
|
+
/** Number of files included */
|
|
15
|
+
fileCount: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a gzipped tarball from a project directory
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTarball(projectDir: string): Promise<TarballResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Read tarball as buffer
|
|
23
|
+
*/
|
|
24
|
+
export declare function readTarball(tarballPath: string): Promise<Buffer>;
|
|
25
|
+
/**
|
|
26
|
+
* Format file size for display
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatSize(bytes: number): string;
|
|
29
|
+
//# sourceMappingURL=tarball.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tarball.d.ts","sourceRoot":"","sources":["../src/tarball.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4FH,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,CAAC,CA8CxB;AAgBD;;GAEG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAStE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD"}
|