klamdo-mcp 1.0.0 → 1.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/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +119 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +11 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -284
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +93 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +224 -0
- package/dist/tools.js.map +1 -0
- package/package.json +5 -3
- package/smithery.yaml +22 -0
- package/src/http.ts +135 -0
- package/src/index.ts +21 -336
- package/src/tools.ts +283 -0
package/src/index.ts
CHANGED
|
@@ -1,360 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Klamdo MCP Server
|
|
3
|
+
* Klamdo MCP Server — stdio transport (for Claude Desktop local use)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* Setup in Claude Desktop config:
|
|
6
|
+
* {
|
|
7
|
+
* "mcpServers": {
|
|
8
|
+
* "klamdo": {
|
|
9
|
+
* "command": "npx",
|
|
10
|
+
* "args": ["klamdo-mcp"],
|
|
11
|
+
* "env": { "KLAMDO_API_KEY": "<your-key-from-klamdo.app/profile>" }
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
-
import {
|
|
19
|
-
CallToolRequestSchema,
|
|
20
|
-
ErrorCode,
|
|
21
|
-
ListResourcesRequestSchema,
|
|
22
|
-
ListToolsRequestSchema,
|
|
23
|
-
McpError,
|
|
24
|
-
ReadResourceRequestSchema
|
|
25
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
19
|
+
import { registerHandlers } from "./tools.js";
|
|
26
20
|
|
|
27
|
-
const BASE_URL = process.env.KLAMDO_BASE_URL ?? "https://klamdo.app";
|
|
28
21
|
const API_KEY = process.env.KLAMDO_API_KEY ?? "";
|
|
29
22
|
|
|
30
23
|
if (!API_KEY) {
|
|
31
24
|
process.stderr.write(
|
|
32
|
-
"[klamdo-mcp] Warning: KLAMDO_API_KEY is not set.
|
|
33
|
-
"Set it in your MCP client config.\n"
|
|
25
|
+
"[klamdo-mcp] Warning: KLAMDO_API_KEY is not set. Set it in your MCP client config.\n"
|
|
34
26
|
);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
async function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"Authorization": `Bearer ${API_KEY}`
|
|
43
|
-
},
|
|
44
|
-
body: body ? JSON.stringify(body) : undefined
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
const text = await res.text().catch(() => "");
|
|
49
|
-
throw new McpError(
|
|
50
|
-
ErrorCode.InternalError,
|
|
51
|
-
`Klamdo API error ${res.status}: ${text.slice(0, 200)}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return res.json() as Promise<T>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
const TOOLS = [
|
|
61
|
-
{
|
|
62
|
-
name: "generate_image",
|
|
63
|
-
description:
|
|
64
|
-
"Generate a 4K identity-locked image using the user's reference photo on Klamdo. " +
|
|
65
|
-
"Returns a job ID — use check_job to poll for the result. Costs 3 credits.",
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: "object",
|
|
68
|
-
properties: {
|
|
69
|
-
prompt: {
|
|
70
|
-
type: "string",
|
|
71
|
-
description:
|
|
72
|
-
"Describe the content you want to generate. Be specific about setting, mood, style, and purpose. " +
|
|
73
|
-
"Example: 'A confident coach standing in a modern co-working space, window light, suited, holding a coffee, editorial style'"
|
|
74
|
-
},
|
|
75
|
-
aspectRatio: {
|
|
76
|
-
type: "string",
|
|
77
|
-
enum: ["1:1", "16:9", "9:16"],
|
|
78
|
-
description: "Output aspect ratio. Default: 1:1 (square, social-optimized).",
|
|
79
|
-
default: "1:1"
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
required: ["prompt"]
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "generate_video",
|
|
87
|
-
description:
|
|
88
|
-
"Generate a 5-second vertical identity-locked video from the user's reference photo on Klamdo. " +
|
|
89
|
-
"Returns a job ID — use check_job to poll for the result. Aspect ratio is locked to 9:16. Costs 6 credits.",
|
|
90
|
-
inputSchema: {
|
|
91
|
-
type: "object",
|
|
92
|
-
properties: {
|
|
93
|
-
prompt: {
|
|
94
|
-
type: "string",
|
|
95
|
-
description:
|
|
96
|
-
"Describe the video content. Include motion cues for best results. " +
|
|
97
|
-
"Example: 'A confident entrepreneur walking through a city street, slow zoom, cinematic lighting'"
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
required: ["prompt"]
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: "check_job",
|
|
105
|
-
description:
|
|
106
|
-
"Check the status of a Klamdo generation job. Returns status and download URLs when complete.",
|
|
107
|
-
inputSchema: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {
|
|
110
|
-
jobId: {
|
|
111
|
-
type: "string",
|
|
112
|
-
description: "The job ID returned from generate_image or generate_video"
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
required: ["jobId"]
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: "get_account",
|
|
120
|
-
description: "Get current Klamdo account status: credit balance, plan, and recent jobs.",
|
|
121
|
-
inputSchema: {
|
|
122
|
-
type: "object",
|
|
123
|
-
properties: {}
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: "list_jobs",
|
|
128
|
-
description: "List recent Klamdo generation jobs for this account.",
|
|
129
|
-
inputSchema: {
|
|
130
|
-
type: "object",
|
|
131
|
-
properties: {
|
|
132
|
-
limit: {
|
|
133
|
-
type: "number",
|
|
134
|
-
description: "Maximum jobs to return (default: 10, max: 50)",
|
|
135
|
-
default: 10
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
const server = new Server(
|
|
145
|
-
{ name: "klamdo", version: "1.0.0" },
|
|
146
|
-
{ capabilities: { tools: {}, resources: {} } }
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
150
|
-
|
|
151
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
152
|
-
const { name, arguments: args } = request.params;
|
|
153
|
-
const input = (args ?? {}) as Record<string, unknown>;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
switch (name) {
|
|
157
|
-
case "generate_image": {
|
|
158
|
-
const result = await klamdo<{ jobId: string; status: string; creditsReserved: number }>("/jobs", {
|
|
159
|
-
prompt: input.prompt,
|
|
160
|
-
mode: "image",
|
|
161
|
-
aspectRatio: input.aspectRatio ?? "4:5"
|
|
162
|
-
});
|
|
163
|
-
return {
|
|
164
|
-
content: [
|
|
165
|
-
{
|
|
166
|
-
type: "text",
|
|
167
|
-
text: `Image generation started.\n\nJob ID: ${result.jobId}\nStatus: ${result.status}\nCredits reserved: ${result.creditsReserved}\n\nUse check_job("${result.jobId}") to get the result. Images typically complete in 30–90 seconds.`
|
|
168
|
-
}
|
|
169
|
-
]
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
case "generate_video": {
|
|
174
|
-
const result = await klamdo<{ jobId: string; status: string; creditsReserved: number }>("/jobs", {
|
|
175
|
-
prompt: input.prompt,
|
|
176
|
-
mode: "video",
|
|
177
|
-
aspectRatio: "9:16"
|
|
178
|
-
});
|
|
179
|
-
return {
|
|
180
|
-
content: [
|
|
181
|
-
{
|
|
182
|
-
type: "text",
|
|
183
|
-
text: `Video generation started.\n\nJob ID: ${result.jobId}\nStatus: ${result.status}\nCredits reserved: ${result.creditsReserved}\n\nUse check_job("${result.jobId}") to get the result. Videos typically complete in 2–5 minutes.`
|
|
184
|
-
}
|
|
185
|
-
]
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
case "check_job": {
|
|
190
|
-
const jobId = String(input.jobId ?? "");
|
|
191
|
-
if (!jobId) throw new McpError(ErrorCode.InvalidParams, "jobId is required");
|
|
192
|
-
|
|
193
|
-
const result = await klamdo<{
|
|
194
|
-
status: string;
|
|
195
|
-
assets: Array<{ kind: string; url: string }>;
|
|
196
|
-
errorMessage?: string;
|
|
197
|
-
caption?: string;
|
|
198
|
-
}>(`/jobs/${jobId}`);
|
|
199
|
-
|
|
200
|
-
if (result.status === "completed") {
|
|
201
|
-
const imageAsset = result.assets.find((a) => a.kind === "image");
|
|
202
|
-
const videoAsset = result.assets.find((a) => a.kind === "video");
|
|
203
|
-
const lines = [
|
|
204
|
-
`Job ${jobId} — Completed ✓`,
|
|
205
|
-
"",
|
|
206
|
-
imageAsset ? `Image URL: ${imageAsset.url}` : null,
|
|
207
|
-
videoAsset ? `Video URL: ${videoAsset.url}` : null,
|
|
208
|
-
result.caption ? `\nSuggested caption:\n${result.caption}` : null,
|
|
209
|
-
"",
|
|
210
|
-
`Share page: ${BASE_URL}/share/${jobId}`
|
|
211
|
-
].filter(Boolean);
|
|
212
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (result.status === "failed") {
|
|
216
|
-
return {
|
|
217
|
-
content: [
|
|
218
|
-
{
|
|
219
|
-
type: "text",
|
|
220
|
-
text: `Job ${jobId} failed: ${result.errorMessage ?? "Unknown error"}. Credits have been refunded.`
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
content: [
|
|
228
|
-
{
|
|
229
|
-
type: "text",
|
|
230
|
-
text: `Job ${jobId} is still running (status: ${result.status}). Check again in 15–30 seconds.`
|
|
231
|
-
}
|
|
232
|
-
]
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
case "get_account": {
|
|
237
|
-
const result = await klamdo<{
|
|
238
|
-
name: string;
|
|
239
|
-
email: string;
|
|
240
|
-
availableCredits: number;
|
|
241
|
-
planTier: string;
|
|
242
|
-
freeSampleEligible: boolean;
|
|
243
|
-
}>("/account");
|
|
244
|
-
return {
|
|
245
|
-
content: [
|
|
246
|
-
{
|
|
247
|
-
type: "text",
|
|
248
|
-
text: [
|
|
249
|
-
`Klamdo Account: ${result.name} (${result.email})`,
|
|
250
|
-
`Plan: ${result.planTier}`,
|
|
251
|
-
`Credits: ${result.availableCredits}`,
|
|
252
|
-
result.freeSampleEligible ? "Free sample: available" : ""
|
|
253
|
-
].filter(Boolean).join("\n")
|
|
254
|
-
}
|
|
255
|
-
]
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
case "list_jobs": {
|
|
260
|
-
const limit = Math.min(Number(input.limit ?? 10), 50);
|
|
261
|
-
const result = await klamdo<{ jobs: Array<{ id: string; status: string; mode: string; prompt: string; createdAt: string }> }>(`/jobs?limit=${limit}`);
|
|
262
|
-
const lines = result.jobs.map(
|
|
263
|
-
(j) => `[${j.status}] ${j.id} — ${j.mode} — "${j.prompt.slice(0, 60)}" (${j.createdAt.slice(0, 10)})`
|
|
264
|
-
);
|
|
265
|
-
return {
|
|
266
|
-
content: [
|
|
267
|
-
{
|
|
268
|
-
type: "text",
|
|
269
|
-
text: lines.length ? lines.join("\n") : "No jobs found."
|
|
270
|
-
}
|
|
271
|
-
]
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
default:
|
|
276
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
277
|
-
}
|
|
278
|
-
} catch (err) {
|
|
279
|
-
if (err instanceof McpError) throw err;
|
|
280
|
-
throw new McpError(
|
|
281
|
-
ErrorCode.InternalError,
|
|
282
|
-
err instanceof Error ? err.message : String(err)
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// Resources: expose the API docs as a readable resource
|
|
288
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
289
|
-
resources: [
|
|
290
|
-
{
|
|
291
|
-
uri: "klamdo://docs",
|
|
292
|
-
name: "Klamdo API Documentation",
|
|
293
|
-
description: "How to use Klamdo's MCP tools and what each parameter does",
|
|
294
|
-
mimeType: "text/plain"
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
|
-
}));
|
|
298
|
-
|
|
299
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
300
|
-
if (request.params.uri === "klamdo://docs") {
|
|
301
|
-
return {
|
|
302
|
-
contents: [
|
|
303
|
-
{
|
|
304
|
-
uri: "klamdo://docs",
|
|
305
|
-
mimeType: "text/plain",
|
|
306
|
-
text: `# Klamdo MCP Server
|
|
307
|
-
|
|
308
|
-
Klamdo generates identity-locked 4K images and 5-second vertical videos for coaches and creator-founders.
|
|
309
|
-
|
|
310
|
-
## Tools
|
|
311
|
-
|
|
312
|
-
### generate_image
|
|
313
|
-
Generate a 4K image from the user's reference photo. Costs 3 credits.
|
|
314
|
-
- prompt (required): Describe the content, setting, mood, and style
|
|
315
|
-
- aspectRatio (optional): "1:1" | "16:9" | "9:16" — default "1:1"
|
|
316
|
-
|
|
317
|
-
### generate_video
|
|
318
|
-
Generate a 5-second 9:16 vertical video. Costs 6 credits.
|
|
319
|
-
- prompt (required): Describe the video content with motion cues
|
|
320
|
-
|
|
321
|
-
### check_job
|
|
322
|
-
Poll a generation job for results.
|
|
323
|
-
- jobId (required): Job ID from generate_image or generate_video
|
|
324
|
-
|
|
325
|
-
### get_account
|
|
326
|
-
Get account status: credits, plan tier, free sample eligibility.
|
|
327
|
-
|
|
328
|
-
### list_jobs
|
|
329
|
-
List recent jobs.
|
|
330
|
-
- limit (optional): Number of jobs to return (default 10, max 50)
|
|
331
|
-
|
|
332
|
-
## Pricing
|
|
333
|
-
- $19.99/month for 120 credits
|
|
334
|
-
- 3 credits per 4K image (~40/mo)
|
|
335
|
-
- 6 credits per 5-sec video (~20/mo)
|
|
336
|
-
- Free sample on first sign-up
|
|
337
|
-
|
|
338
|
-
## Links
|
|
339
|
-
- Sign up: https://klamdo.app/sign-up
|
|
340
|
-
- Pricing: https://klamdo.app/pricing
|
|
341
|
-
`
|
|
342
|
-
}
|
|
343
|
-
]
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${request.params.uri}`);
|
|
347
|
-
});
|
|
29
|
+
async function main() {
|
|
30
|
+
const server = new Server(
|
|
31
|
+
{ name: "klamdo", version: "1.1.0" },
|
|
32
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
33
|
+
);
|
|
348
34
|
|
|
349
|
-
|
|
35
|
+
registerHandlers(server, () => API_KEY);
|
|
350
36
|
|
|
351
|
-
async function main() {
|
|
352
37
|
const transport = new StdioServerTransport();
|
|
353
38
|
await server.connect(transport);
|
|
354
|
-
process.stderr.write("[klamdo-mcp]
|
|
39
|
+
process.stderr.write("[klamdo-mcp] Running via stdio\n");
|
|
355
40
|
}
|
|
356
41
|
|
|
357
42
|
main().catch((err) => {
|
|
358
|
-
process.stderr.write(`[klamdo-mcp] Fatal
|
|
43
|
+
process.stderr.write(`[klamdo-mcp] Fatal: ${err}\n`);
|
|
359
44
|
process.exit(1);
|
|
360
45
|
});
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool definitions and implementations for both stdio and HTTP transports.
|
|
3
|
+
* The apiKey is per-request in HTTP mode, per-process in stdio mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
McpError,
|
|
12
|
+
ReadResourceRequestSchema
|
|
13
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
|
|
16
|
+
export const BASE_URL = process.env.KLAMDO_BASE_URL ?? "https://klamdo.app";
|
|
17
|
+
|
|
18
|
+
export async function klamdo<T>(
|
|
19
|
+
path: string,
|
|
20
|
+
apiKey: string,
|
|
21
|
+
body?: Record<string, unknown>
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const res = await fetch(`${BASE_URL}/api/mcp${path}`, {
|
|
24
|
+
method: body ? "POST" : "GET",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Authorization: `Bearer ${apiKey}`
|
|
28
|
+
},
|
|
29
|
+
body: body ? JSON.stringify(body) : undefined
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => "");
|
|
34
|
+
if (res.status === 401) {
|
|
35
|
+
throw new McpError(
|
|
36
|
+
ErrorCode.InvalidRequest,
|
|
37
|
+
"Invalid API key. Get your key at https://klamdo.app/profile"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
throw new McpError(
|
|
41
|
+
ErrorCode.InternalError,
|
|
42
|
+
`Klamdo API error ${res.status}: ${text.slice(0, 200)}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return res.json() as Promise<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const TOOLS = [
|
|
50
|
+
{
|
|
51
|
+
name: "generate_image",
|
|
52
|
+
description:
|
|
53
|
+
"Generate a 4K identity-locked image using the user's reference photo on Klamdo. " +
|
|
54
|
+
"Returns a job ID — use check_job to poll for the result. Costs 3 credits.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
prompt: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description:
|
|
61
|
+
"Describe the content you want to generate. Be specific about setting, mood, style, and purpose."
|
|
62
|
+
},
|
|
63
|
+
aspectRatio: {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: ["1:1", "16:9", "9:16"],
|
|
66
|
+
description: "Output aspect ratio. Default: 1:1 (square).",
|
|
67
|
+
default: "1:1"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
required: ["prompt"]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "generate_video",
|
|
75
|
+
description:
|
|
76
|
+
"Generate a 5-second vertical identity-locked video from the user's reference photo on Klamdo. " +
|
|
77
|
+
"Returns a job ID — use check_job to poll. Aspect ratio locked to 9:16. Costs 6 credits.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
prompt: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Describe the video content with motion cues for best results."
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: ["prompt"]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "check_job",
|
|
91
|
+
description: "Check the status of a Klamdo generation job. Returns status and download URLs when complete.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
jobId: { type: "string", description: "The job ID returned from generate_image or generate_video" }
|
|
96
|
+
},
|
|
97
|
+
required: ["jobId"]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_account",
|
|
102
|
+
description: "Get current Klamdo account status: credit balance, plan, and free sample eligibility.",
|
|
103
|
+
inputSchema: { type: "object", properties: {} }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "list_jobs",
|
|
107
|
+
description: "List recent Klamdo generation jobs for this account.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
limit: { type: "number", description: "Maximum jobs to return (default: 10, max: 50)", default: 10 }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
export function registerHandlers(server: Server, getApiKey: () => string) {
|
|
118
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
119
|
+
|
|
120
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
|
+
const { name, arguments: args } = request.params;
|
|
122
|
+
const input = (args ?? {}) as Record<string, unknown>;
|
|
123
|
+
const apiKey = getApiKey();
|
|
124
|
+
|
|
125
|
+
if (!apiKey) {
|
|
126
|
+
throw new McpError(
|
|
127
|
+
ErrorCode.InvalidRequest,
|
|
128
|
+
"No API key provided. Set KLAMDO_API_KEY or pass it via Authorization header."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
switch (name) {
|
|
134
|
+
case "generate_image": {
|
|
135
|
+
const result = await klamdo<{ jobId: string; status: string; creditsReserved: number }>(
|
|
136
|
+
"/jobs",
|
|
137
|
+
apiKey,
|
|
138
|
+
{ prompt: input.prompt, mode: "image", aspectRatio: input.aspectRatio ?? "1:1" }
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Image generation started.\n\nJob ID: ${result.jobId}\nStatus: ${result.status}\nCredits reserved: ${result.creditsReserved}\n\nUse check_job("${result.jobId}") to get the result. Images typically complete in 30–90 seconds.`
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "generate_video": {
|
|
151
|
+
const result = await klamdo<{ jobId: string; status: string; creditsReserved: number }>(
|
|
152
|
+
"/jobs",
|
|
153
|
+
apiKey,
|
|
154
|
+
{ prompt: input.prompt, mode: "video", aspectRatio: "9:16" }
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: `Video generation started.\n\nJob ID: ${result.jobId}\nStatus: ${result.status}\nCredits reserved: ${result.creditsReserved}\n\nUse check_job("${result.jobId}") to get the result. Videos typically complete in 2–5 minutes.`
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "check_job": {
|
|
167
|
+
const jobId = String(input.jobId ?? "");
|
|
168
|
+
if (!jobId) throw new McpError(ErrorCode.InvalidParams, "jobId is required");
|
|
169
|
+
|
|
170
|
+
const result = await klamdo<{
|
|
171
|
+
status: string;
|
|
172
|
+
assets: Array<{ kind: string; url: string }>;
|
|
173
|
+
errorMessage?: string;
|
|
174
|
+
caption?: string;
|
|
175
|
+
}>(`/jobs/${jobId}`, apiKey);
|
|
176
|
+
|
|
177
|
+
if (result.status === "completed") {
|
|
178
|
+
const imageAsset = result.assets.find((a) => a.kind === "image");
|
|
179
|
+
const videoAsset = result.assets.find((a) => a.kind === "video");
|
|
180
|
+
const lines = [
|
|
181
|
+
`Job ${jobId} — Completed ✓`,
|
|
182
|
+
"",
|
|
183
|
+
imageAsset ? `Image URL: ${imageAsset.url}` : null,
|
|
184
|
+
videoAsset ? `Video URL: ${videoAsset.url}` : null,
|
|
185
|
+
result.caption ? `\nSuggested caption:\n${result.caption}` : null,
|
|
186
|
+
"",
|
|
187
|
+
`Share page: ${BASE_URL}/share/${jobId}`
|
|
188
|
+
].filter(Boolean);
|
|
189
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (result.status === "failed") {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `Job ${jobId} failed: ${result.errorMessage ?? "Unknown error"}. Credits have been refunded.`
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `Job ${jobId} is still running (status: ${result.status}). Check again in 15–30 seconds.`
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case "get_account": {
|
|
214
|
+
const result = await klamdo<{
|
|
215
|
+
name: string;
|
|
216
|
+
email: string;
|
|
217
|
+
availableCredits: number;
|
|
218
|
+
planTier: string;
|
|
219
|
+
freeSampleEligible: boolean;
|
|
220
|
+
}>("/account", apiKey);
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: [
|
|
226
|
+
`Klamdo Account: ${result.name} (${result.email})`,
|
|
227
|
+
`Plan: ${result.planTier}`,
|
|
228
|
+
`Credits: ${result.availableCredits}`,
|
|
229
|
+
result.freeSampleEligible ? "Free sample: available" : ""
|
|
230
|
+
].filter(Boolean).join("\n")
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case "list_jobs": {
|
|
237
|
+
const limit = Math.min(Number(input.limit ?? 10), 50);
|
|
238
|
+
const result = await klamdo<{
|
|
239
|
+
jobs: Array<{ id: string; status: string; mode: string; prompt: string; createdAt: string }>;
|
|
240
|
+
}>(`/jobs?limit=${limit}`, apiKey);
|
|
241
|
+
const lines = result.jobs.map(
|
|
242
|
+
(j) => `[${j.status}] ${j.id} — ${j.mode} — "${j.prompt.slice(0, 60)}" (${j.createdAt.slice(0, 10)})`
|
|
243
|
+
);
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: lines.length ? lines.join("\n") : "No jobs found." }]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof McpError) throw err;
|
|
254
|
+
throw new McpError(ErrorCode.InternalError, err instanceof Error ? err.message : String(err));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
259
|
+
resources: [
|
|
260
|
+
{
|
|
261
|
+
uri: "klamdo://docs",
|
|
262
|
+
name: "Klamdo API Documentation",
|
|
263
|
+
description: "How to use Klamdo's MCP tools",
|
|
264
|
+
mimeType: "text/plain"
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
270
|
+
if (request.params.uri === "klamdo://docs") {
|
|
271
|
+
return {
|
|
272
|
+
contents: [
|
|
273
|
+
{
|
|
274
|
+
uri: "klamdo://docs",
|
|
275
|
+
mimeType: "text/plain",
|
|
276
|
+
text: `# Klamdo MCP Server\n\nTools: generate_image, generate_video, check_job, get_account, list_jobs\n\nGet your API key at https://klamdo.app/profile\nPricing: $19.99/mo for 120 credits (3/image, 6/video)\n`
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${request.params.uri}`);
|
|
282
|
+
});
|
|
283
|
+
}
|