@icogenie/mcp 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/CHANGELOG.md +18 -0
- package/README.md +209 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +519 -0
- package/package.json +68 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2025-01-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- `generate_icon` tool - Generate single icon preview (1 credit)
|
|
10
|
+
- `regenerate_icon` tool - Regenerate variation with custom prompt (1 credit)
|
|
11
|
+
- `check_credits` tool - Check credit balance (free)
|
|
12
|
+
- `download_icon` tool - Download SVG/PNG package (5 credits single, 4/icon bundle)
|
|
13
|
+
- `normalize_bundle` tool - Plan bundle icon list (free, rate-limited)
|
|
14
|
+
- `generate_bundle` tool - Generate bundle from icon list (1 credit/icon)
|
|
15
|
+
- Browser-based OAuth authentication (shared with @icogenie/cli)
|
|
16
|
+
- Reference image support for style extraction
|
|
17
|
+
- CI/CD support via `ICOGENIE_SESSION_TOKEN` environment variable
|
|
18
|
+
- Stdio transport for Claude Desktop and Cursor integration
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @icogenie/mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for IcoGenie. Enables AI agents like Claude to generate production-ready SVG icons programmatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @icogenie/mcp
|
|
9
|
+
# or use directly with npx
|
|
10
|
+
npx @icogenie/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
### Claude Desktop
|
|
16
|
+
|
|
17
|
+
Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"icogenie": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "@icogenie/mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Claude Code
|
|
31
|
+
|
|
32
|
+
The server works automatically when installed globally or via npx.
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
On first use, the MCP server will:
|
|
37
|
+
1. Open your browser for authentication
|
|
38
|
+
2. Ask you to approve access
|
|
39
|
+
3. Save the session token to `~/.icogenie/config.json`
|
|
40
|
+
|
|
41
|
+
Subsequent uses are automatic - the token is shared with the [IcoGenie CLI](https://www.npmjs.com/package/@icogenie/cli).
|
|
42
|
+
|
|
43
|
+
### CI/CD Environments
|
|
44
|
+
|
|
45
|
+
Set `ICOGENIE_SESSION_TOKEN` environment variable to skip browser authentication:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export ICOGENIE_SESSION_TOKEN="your-session-token"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Available Tools
|
|
52
|
+
|
|
53
|
+
### generate_icon
|
|
54
|
+
Generate a single icon preview from a text description.
|
|
55
|
+
|
|
56
|
+
**Cost:** 1 credit
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
generate_icon({
|
|
60
|
+
prompt: "home icon",
|
|
61
|
+
style: "solid", // or "outline"
|
|
62
|
+
variations: 1, // 1, 2, or 4
|
|
63
|
+
referenceImagePath: "/path/to/reference.png" // optional
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Returns:** `{ generationId, preview, creditsUsed, creditsRemaining, metadata }`
|
|
68
|
+
|
|
69
|
+
### regenerate_icon
|
|
70
|
+
Regenerate a specific icon variation with a custom refinement prompt.
|
|
71
|
+
|
|
72
|
+
**Cost:** 1 credit
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
regenerate_icon({
|
|
76
|
+
generationId: "abc123", // or bundleId for bundles
|
|
77
|
+
index: 0, // which variation (0-based)
|
|
78
|
+
prompt: "Make it more 3D" // optional refinement
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### check_credits
|
|
83
|
+
Check your current credit balance.
|
|
84
|
+
|
|
85
|
+
**Cost:** Free
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
check_credits()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Returns:** `{ credits, team, user }`
|
|
92
|
+
|
|
93
|
+
### download_icon
|
|
94
|
+
Download the final SVG + PNG package for an icon or bundle.
|
|
95
|
+
|
|
96
|
+
**Cost:** 5 credits (single) or 4 credits/icon (bundle)
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
download_icon({
|
|
100
|
+
generationId: "abc123", // or bundleId
|
|
101
|
+
outputPath: "./icons.zip" // optional, returns base64 if omitted
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### normalize_bundle
|
|
106
|
+
Plan an icon bundle by generating an AI-enhanced icon list from a description.
|
|
107
|
+
|
|
108
|
+
**Cost:** Free (rate-limited)
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
normalize_bundle({
|
|
112
|
+
description: "food delivery app",
|
|
113
|
+
targetCount: 10,
|
|
114
|
+
style: "solid"
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Returns:** `{ icons, bundleType, styleRecommendation, reasoning }`
|
|
119
|
+
|
|
120
|
+
### generate_bundle
|
|
121
|
+
Generate a bundle of icons from an icon list.
|
|
122
|
+
|
|
123
|
+
**Cost:** 1 credit per icon
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
generate_bundle({
|
|
127
|
+
icons: [
|
|
128
|
+
{ name: "home", description: "Home navigation icon" },
|
|
129
|
+
{ name: "cart", description: "Shopping cart icon" }
|
|
130
|
+
],
|
|
131
|
+
style: "solid"
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Returns:** `{ bundleId, icons: [{ name, preview }], credits }`
|
|
136
|
+
|
|
137
|
+
## Example Workflow
|
|
138
|
+
|
|
139
|
+
1. **Check credits:**
|
|
140
|
+
```
|
|
141
|
+
check_credits()
|
|
142
|
+
→ { credits: 50, team: { name: "Personal" } }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
2. **Generate a single icon:**
|
|
146
|
+
```
|
|
147
|
+
generate_icon({ prompt: "notification bell icon", style: "outline" })
|
|
148
|
+
→ { generationId: "abc123", preview: "...", creditsRemaining: 49 }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
3. **Refine if needed:**
|
|
152
|
+
```
|
|
153
|
+
regenerate_icon({ generationId: "abc123", index: 0, prompt: "Add a dot indicator" })
|
|
154
|
+
→ { preview: "...", creditsRemaining: 48 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
4. **Download final package:**
|
|
158
|
+
```
|
|
159
|
+
download_icon({ generationId: "abc123", outputPath: "./bell-icon.zip" })
|
|
160
|
+
→ { savedTo: "./bell-icon.zip", creditsRemaining: 43 }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Bundle Workflow
|
|
164
|
+
|
|
165
|
+
1. **Plan the bundle (free):**
|
|
166
|
+
```
|
|
167
|
+
normalize_bundle({ description: "e-commerce app icons", targetCount: 8 })
|
|
168
|
+
→ { icons: [{ name: "cart", ... }, ...], styleRecommendation: "outline" }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
2. **Review and generate:**
|
|
172
|
+
```
|
|
173
|
+
generate_bundle({ icons: [...], style: "outline" })
|
|
174
|
+
→ { bundleId: "xyz789", icons: [...], credits: { previewUsed: 8 } }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. **Download bundle:**
|
|
178
|
+
```
|
|
179
|
+
download_icon({ bundleId: "xyz789", outputPath: "./ecommerce-icons.zip" })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Environment Variables
|
|
183
|
+
|
|
184
|
+
| Variable | Description | Default |
|
|
185
|
+
|----------|-------------|---------|
|
|
186
|
+
| `ICOGENIE_API_URL` | API endpoint | `https://icogenie.com` |
|
|
187
|
+
| `ICOGENIE_SESSION_TOKEN` | Session token (for CI/CD) | - |
|
|
188
|
+
| `ICOGENIE_CONFIG_DIR` | Config directory | `~/.icogenie` |
|
|
189
|
+
|
|
190
|
+
## Credit Pricing
|
|
191
|
+
|
|
192
|
+
| Action | Cost |
|
|
193
|
+
|--------|------|
|
|
194
|
+
| Preview (single) | 1 credit |
|
|
195
|
+
| Preview (bundle) | 1 credit/icon |
|
|
196
|
+
| Download (single) | 5 credits |
|
|
197
|
+
| Download (bundle) | 4 credits/icon |
|
|
198
|
+
| Regenerate | 1 credit |
|
|
199
|
+
|
|
200
|
+
Purchase credits at [icogenie.com](https://icogenie.com).
|
|
201
|
+
|
|
202
|
+
## Related
|
|
203
|
+
|
|
204
|
+
- [@icogenie/cli](https://www.npmjs.com/package/@icogenie/cli) - Command-line interface
|
|
205
|
+
- [IcoGenie Web](https://icogenie.com) - Web application
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
|
|
9
|
+
// src/tools/generate.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
// src/api/client.ts
|
|
13
|
+
import { readFileSync, statSync } from "fs";
|
|
14
|
+
import { extname } from "path";
|
|
15
|
+
|
|
16
|
+
// src/auth/config.ts
|
|
17
|
+
import Conf from "conf";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { mkdirSync, existsSync } from "fs";
|
|
21
|
+
var configDir = process.env.ICOGENIE_CONFIG_DIR || join(homedir(), ".icogenie");
|
|
22
|
+
if (!existsSync(configDir)) {
|
|
23
|
+
mkdirSync(configDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
var config = new Conf({
|
|
26
|
+
projectName: "icogenie",
|
|
27
|
+
cwd: configDir,
|
|
28
|
+
defaults: {
|
|
29
|
+
apiUrl: "https://icogenie.com"
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
function getApiUrl() {
|
|
33
|
+
return process.env.ICOGENIE_API_URL || config.get("apiUrl");
|
|
34
|
+
}
|
|
35
|
+
function getCredentials() {
|
|
36
|
+
const envToken = process.env.ICOGENIE_SESSION_TOKEN;
|
|
37
|
+
if (envToken) {
|
|
38
|
+
return {
|
|
39
|
+
sessionToken: envToken,
|
|
40
|
+
userId: "env-user",
|
|
41
|
+
userEmail: "env@icogenie.com",
|
|
42
|
+
teamId: "env-team",
|
|
43
|
+
teamName: "Environment",
|
|
44
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return config.get("credentials");
|
|
48
|
+
}
|
|
49
|
+
function setCredentials(credentials) {
|
|
50
|
+
config.set("credentials", credentials);
|
|
51
|
+
}
|
|
52
|
+
function clearCredentials() {
|
|
53
|
+
config.delete("credentials");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/auth/flow.ts
|
|
57
|
+
import open from "open";
|
|
58
|
+
async function request(endpoint, body) {
|
|
59
|
+
const url = `${getApiUrl()}/api/cli${endpoint}`;
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body)
|
|
64
|
+
});
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(data.error || data.message || "Request failed");
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
async function startAuth() {
|
|
72
|
+
return request("/auth/start", {});
|
|
73
|
+
}
|
|
74
|
+
async function pollAuth(pollToken) {
|
|
75
|
+
return request("/auth/poll", { pollToken });
|
|
76
|
+
}
|
|
77
|
+
async function authenticate() {
|
|
78
|
+
const { pollToken, loginUrl, expiresInSeconds } = await startAuth();
|
|
79
|
+
await open(loginUrl);
|
|
80
|
+
const pollInterval = 2e3;
|
|
81
|
+
const maxAttempts = Math.floor(expiresInSeconds * 1e3 / pollInterval);
|
|
82
|
+
let attempts = 0;
|
|
83
|
+
while (attempts < maxAttempts) {
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
85
|
+
attempts++;
|
|
86
|
+
const result = await pollAuth(pollToken);
|
|
87
|
+
if (result.status === "approved" && result.sessionToken && result.user && result.team) {
|
|
88
|
+
const credentials = {
|
|
89
|
+
sessionToken: result.sessionToken,
|
|
90
|
+
userId: result.user.id,
|
|
91
|
+
userEmail: result.user.email,
|
|
92
|
+
userName: result.user.name,
|
|
93
|
+
teamId: result.team.id,
|
|
94
|
+
teamName: result.team.name,
|
|
95
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
96
|
+
};
|
|
97
|
+
setCredentials(credentials);
|
|
98
|
+
return credentials;
|
|
99
|
+
}
|
|
100
|
+
if (result.status === "expired") {
|
|
101
|
+
throw new Error("Authentication request expired. Please try again.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/auth/middleware.ts
|
|
108
|
+
var AuthError = class extends Error {
|
|
109
|
+
constructor(message, code) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.code = code;
|
|
112
|
+
this.name = "AuthError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
async function ensureAuth() {
|
|
116
|
+
let credentials = getCredentials();
|
|
117
|
+
if (!credentials) {
|
|
118
|
+
try {
|
|
119
|
+
credentials = await authenticate();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new AuthError(
|
|
122
|
+
`Authentication required. Please approve access in your browser. Error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
123
|
+
"AUTH_FAILED"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return credentials;
|
|
128
|
+
}
|
|
129
|
+
async function handleAuthError() {
|
|
130
|
+
clearCredentials();
|
|
131
|
+
try {
|
|
132
|
+
return await authenticate();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new AuthError(
|
|
135
|
+
"Session expired. Please approve access in your browser.",
|
|
136
|
+
"SESSION_EXPIRED"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/api/client.ts
|
|
142
|
+
var ApiError = class extends Error {
|
|
143
|
+
constructor(message, statusCode, details) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.statusCode = statusCode;
|
|
146
|
+
this.details = details;
|
|
147
|
+
this.name = "ApiError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var SUPPORTED_IMAGE_FORMATS = [".png", ".jpg", ".jpeg", ".webp"];
|
|
151
|
+
var MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
152
|
+
function readReferenceImage(filePath) {
|
|
153
|
+
const ext = extname(filePath).toLowerCase();
|
|
154
|
+
if (!SUPPORTED_IMAGE_FORMATS.includes(ext)) {
|
|
155
|
+
throw new Error(`Unsupported image format: ${ext}. Supported: ${SUPPORTED_IMAGE_FORMATS.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
const stats = statSync(filePath);
|
|
158
|
+
if (stats.size > MAX_IMAGE_SIZE) {
|
|
159
|
+
throw new Error(`Image too large: ${(stats.size / 1024 / 1024).toFixed(1)}MB. Max: 5MB`);
|
|
160
|
+
}
|
|
161
|
+
const buffer = readFileSync(filePath);
|
|
162
|
+
const mimeType = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
|
|
163
|
+
return { data: buffer.toString("base64"), mimeType };
|
|
164
|
+
}
|
|
165
|
+
async function request2(endpoint, body, requireAuth = true) {
|
|
166
|
+
const url = `${getApiUrl()}/api/cli${endpoint}`;
|
|
167
|
+
const makeRequest = async (sessionToken) => {
|
|
168
|
+
const requestBody = sessionToken ? { ...body, sessionToken } : body;
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
body: JSON.stringify(requestBody)
|
|
173
|
+
});
|
|
174
|
+
const data = await response.json();
|
|
175
|
+
if (response.status === 401 && requireAuth) {
|
|
176
|
+
const newCreds = await handleAuthError();
|
|
177
|
+
return makeRequest(newCreds.sessionToken);
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new ApiError(
|
|
181
|
+
data.error || data.message || "Request failed",
|
|
182
|
+
response.status,
|
|
183
|
+
data
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return data;
|
|
187
|
+
};
|
|
188
|
+
if (requireAuth) {
|
|
189
|
+
const credentials = await ensureAuth();
|
|
190
|
+
return makeRequest(credentials.sessionToken);
|
|
191
|
+
}
|
|
192
|
+
return makeRequest();
|
|
193
|
+
}
|
|
194
|
+
async function generate(options) {
|
|
195
|
+
return request2("/generate", {
|
|
196
|
+
prompt: options.prompt,
|
|
197
|
+
variations: options.variations || 1,
|
|
198
|
+
style: options.style || "solid",
|
|
199
|
+
...options.referenceImage && { referenceImage: options.referenceImage }
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function regenerate(options) {
|
|
203
|
+
return request2("/regenerate", {
|
|
204
|
+
generationId: options.generationId,
|
|
205
|
+
bundleId: options.bundleId,
|
|
206
|
+
index: options.index,
|
|
207
|
+
prompt: options.prompt
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function getCredits() {
|
|
211
|
+
return request2("/credits", {});
|
|
212
|
+
}
|
|
213
|
+
async function download(options) {
|
|
214
|
+
return request2("/download", {
|
|
215
|
+
generationId: options.generationId,
|
|
216
|
+
bundleId: options.bundleId
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async function normalizeBundle(options) {
|
|
220
|
+
return request2("/normalize", {
|
|
221
|
+
description: options.description,
|
|
222
|
+
targetCount: options.targetCount,
|
|
223
|
+
style: options.style
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function generateBundle(options) {
|
|
227
|
+
return request2("/bundle", {
|
|
228
|
+
description: options.description,
|
|
229
|
+
targetCount: options.targetCount,
|
|
230
|
+
icons: options.icons,
|
|
231
|
+
style: options.style || "solid",
|
|
232
|
+
...options.referenceImage && { referenceImage: options.referenceImage }
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/tools/generate.ts
|
|
237
|
+
var generateIconSchema = {
|
|
238
|
+
prompt: z.string().describe("Description of the icon to generate"),
|
|
239
|
+
style: z.enum(["solid", "outline"]).default("solid").describe("Icon style"),
|
|
240
|
+
variations: z.union([z.literal(1), z.literal(2), z.literal(4)]).default(1).describe("Number of variations (1, 2, or 4)"),
|
|
241
|
+
referenceImagePath: z.string().optional().describe("Local file path to reference image for style extraction"),
|
|
242
|
+
referenceImage: z.object({
|
|
243
|
+
data: z.string().describe("Base64-encoded image data"),
|
|
244
|
+
mimeType: z.enum(["image/png", "image/jpeg", "image/webp"])
|
|
245
|
+
}).optional().describe("Base64-encoded reference image")
|
|
246
|
+
};
|
|
247
|
+
async function generateIcon(args) {
|
|
248
|
+
let refImage;
|
|
249
|
+
if (args.referenceImagePath) {
|
|
250
|
+
refImage = readReferenceImage(args.referenceImagePath);
|
|
251
|
+
} else if (args.referenceImage) {
|
|
252
|
+
refImage = args.referenceImage;
|
|
253
|
+
}
|
|
254
|
+
const result = await generate({
|
|
255
|
+
prompt: args.prompt,
|
|
256
|
+
style: args.style || "solid",
|
|
257
|
+
variations: args.variations || 1,
|
|
258
|
+
referenceImage: refImage
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
generationId: result.generationId,
|
|
262
|
+
preview: result.preview,
|
|
263
|
+
creditsUsed: result.creditsUsed,
|
|
264
|
+
creditsRemaining: result.creditsRemaining,
|
|
265
|
+
metadata: result.metadata
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/tools/regenerate.ts
|
|
270
|
+
import { z as z2 } from "zod";
|
|
271
|
+
var regenerateIconSchema = {
|
|
272
|
+
generationId: z2.string().optional().describe("For single icon variations"),
|
|
273
|
+
bundleId: z2.string().optional().describe("For bundle icons"),
|
|
274
|
+
index: z2.number().describe("Which variation/icon to regenerate (0-based)"),
|
|
275
|
+
prompt: z2.string().optional().describe("Custom refinement prompt")
|
|
276
|
+
};
|
|
277
|
+
async function regenerateIcon(args) {
|
|
278
|
+
if (!args.generationId && !args.bundleId) {
|
|
279
|
+
throw new Error("Must provide either generationId or bundleId");
|
|
280
|
+
}
|
|
281
|
+
const result = await regenerate({
|
|
282
|
+
generationId: args.generationId,
|
|
283
|
+
bundleId: args.bundleId,
|
|
284
|
+
index: args.index,
|
|
285
|
+
prompt: args.prompt
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
success: result.success,
|
|
289
|
+
preview: result.preview,
|
|
290
|
+
creditsUsed: result.creditsUsed,
|
|
291
|
+
creditsRemaining: result.creditsRemaining
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/tools/credits.ts
|
|
296
|
+
var checkCreditsSchema = {};
|
|
297
|
+
async function checkCredits() {
|
|
298
|
+
const result = await getCredits();
|
|
299
|
+
return {
|
|
300
|
+
credits: result.credits,
|
|
301
|
+
team: result.team,
|
|
302
|
+
user: result.user
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/tools/download.ts
|
|
307
|
+
import { z as z3 } from "zod";
|
|
308
|
+
import { writeFileSync } from "fs";
|
|
309
|
+
var downloadIconSchema = {
|
|
310
|
+
generationId: z3.string().optional().describe("ID from generate_icon result (for single icons)"),
|
|
311
|
+
bundleId: z3.string().optional().describe("ID from generate_bundle result (for bundles)"),
|
|
312
|
+
outputPath: z3.string().optional().describe("Where to save the ZIP file")
|
|
313
|
+
};
|
|
314
|
+
async function downloadIcon(args) {
|
|
315
|
+
if (!args.generationId && !args.bundleId) {
|
|
316
|
+
throw new Error("Must provide either generationId or bundleId");
|
|
317
|
+
}
|
|
318
|
+
const result = await download({
|
|
319
|
+
generationId: args.generationId,
|
|
320
|
+
bundleId: args.bundleId
|
|
321
|
+
});
|
|
322
|
+
const response = {
|
|
323
|
+
success: result.success,
|
|
324
|
+
creditsUsed: result.creditsUsed,
|
|
325
|
+
creditsRemaining: result.creditsRemaining,
|
|
326
|
+
iconCount: result.iconCount
|
|
327
|
+
};
|
|
328
|
+
if (args.outputPath) {
|
|
329
|
+
const buffer = Buffer.from(result.bundle.data, "base64");
|
|
330
|
+
writeFileSync(args.outputPath, buffer);
|
|
331
|
+
response.savedTo = args.outputPath;
|
|
332
|
+
} else {
|
|
333
|
+
response.bundle = {
|
|
334
|
+
data: result.bundle.data,
|
|
335
|
+
filename: result.bundle.filename
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return response;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/tools/normalize-bundle.ts
|
|
342
|
+
import { z as z4 } from "zod";
|
|
343
|
+
var normalizeBundleSchema = {
|
|
344
|
+
description: z4.string().describe("Description of the icon bundle to create"),
|
|
345
|
+
targetCount: z4.number().optional().describe("Target number of icons (2-20)"),
|
|
346
|
+
style: z4.enum(["solid", "outline"]).optional().describe("Preferred icon style")
|
|
347
|
+
};
|
|
348
|
+
async function normalizeBundleTool(args) {
|
|
349
|
+
const result = await normalizeBundle({
|
|
350
|
+
description: args.description,
|
|
351
|
+
targetCount: args.targetCount,
|
|
352
|
+
style: args.style
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
icons: result.icons,
|
|
356
|
+
bundleType: result.bundleType,
|
|
357
|
+
suggestedCount: result.suggestedCount,
|
|
358
|
+
styleRecommendation: result.styleRecommendation,
|
|
359
|
+
reasoning: result.reasoning
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/tools/generate-bundle.ts
|
|
364
|
+
import { z as z5 } from "zod";
|
|
365
|
+
var generateBundleSchema = {
|
|
366
|
+
icons: z5.array(
|
|
367
|
+
z5.object({
|
|
368
|
+
name: z5.string().regex(/^[a-z0-9-]+$/).describe("Icon name in kebab-case"),
|
|
369
|
+
description: z5.string().describe("Icon description")
|
|
370
|
+
})
|
|
371
|
+
).optional().describe("Icon list from normalize_bundle or custom"),
|
|
372
|
+
description: z5.string().optional().describe("Alternative: describe the bundle to auto-generate icon list"),
|
|
373
|
+
targetCount: z5.number().optional().describe("Target icon count when using description"),
|
|
374
|
+
style: z5.enum(["solid", "outline"]).default("solid").describe("Icon style"),
|
|
375
|
+
referenceImagePath: z5.string().optional().describe("Local file path to reference image"),
|
|
376
|
+
referenceImage: z5.object({
|
|
377
|
+
data: z5.string(),
|
|
378
|
+
mimeType: z5.enum(["image/png", "image/jpeg", "image/webp"])
|
|
379
|
+
}).optional().describe("Base64-encoded reference image")
|
|
380
|
+
};
|
|
381
|
+
async function generateBundleTool(args) {
|
|
382
|
+
if (!args.icons && !args.description) {
|
|
383
|
+
throw new Error("Must provide either icons array or description");
|
|
384
|
+
}
|
|
385
|
+
let refImage;
|
|
386
|
+
if (args.referenceImagePath) {
|
|
387
|
+
refImage = readReferenceImage(args.referenceImagePath);
|
|
388
|
+
} else if (args.referenceImage) {
|
|
389
|
+
refImage = args.referenceImage;
|
|
390
|
+
}
|
|
391
|
+
const result = await generateBundle({
|
|
392
|
+
icons: args.icons,
|
|
393
|
+
description: args.description,
|
|
394
|
+
targetCount: args.targetCount,
|
|
395
|
+
style: args.style || "solid",
|
|
396
|
+
referenceImage: refImage
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
bundleId: result.bundleId,
|
|
400
|
+
iconCount: result.iconCount,
|
|
401
|
+
icons: result.icons.map((icon) => ({
|
|
402
|
+
name: icon.name,
|
|
403
|
+
description: icon.description,
|
|
404
|
+
preview: icon.preview
|
|
405
|
+
})),
|
|
406
|
+
credits: result.credits,
|
|
407
|
+
nextStep: result.nextStep
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/server.ts
|
|
412
|
+
function createServer() {
|
|
413
|
+
const server = new McpServer({
|
|
414
|
+
name: "icogenie",
|
|
415
|
+
version: "0.1.0"
|
|
416
|
+
});
|
|
417
|
+
server.registerTool(
|
|
418
|
+
"generate_icon",
|
|
419
|
+
{
|
|
420
|
+
title: "Generate Icon",
|
|
421
|
+
description: "Generate an AI-powered icon preview from a text description. Costs 1 credit. Returns a generationId for download.",
|
|
422
|
+
inputSchema: generateIconSchema
|
|
423
|
+
},
|
|
424
|
+
async (args) => {
|
|
425
|
+
const result = await generateIcon(args);
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
428
|
+
structuredContent: result
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
server.registerTool(
|
|
433
|
+
"regenerate_icon",
|
|
434
|
+
{
|
|
435
|
+
title: "Regenerate Icon",
|
|
436
|
+
description: "Regenerate a specific icon variation with an optional custom prompt. Costs 1 credit. Use with generationId (single) or bundleId (bundle).",
|
|
437
|
+
inputSchema: regenerateIconSchema
|
|
438
|
+
},
|
|
439
|
+
async (args) => {
|
|
440
|
+
const result = await regenerateIcon(args);
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
443
|
+
structuredContent: result
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
server.registerTool(
|
|
448
|
+
"check_credits",
|
|
449
|
+
{
|
|
450
|
+
title: "Check Credits",
|
|
451
|
+
description: "Check the current credit balance and account information. Free to use.",
|
|
452
|
+
inputSchema: checkCreditsSchema
|
|
453
|
+
},
|
|
454
|
+
async () => {
|
|
455
|
+
const result = await checkCredits();
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
458
|
+
structuredContent: result
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
server.registerTool(
|
|
463
|
+
"download_icon",
|
|
464
|
+
{
|
|
465
|
+
title: "Download Icon",
|
|
466
|
+
description: "Download the final SVG + PNG package for an icon. Costs 5 credits (single) or 4 credits/icon (bundle). Provide outputPath to save to file.",
|
|
467
|
+
inputSchema: downloadIconSchema
|
|
468
|
+
},
|
|
469
|
+
async (args) => {
|
|
470
|
+
const result = await downloadIcon(args);
|
|
471
|
+
return {
|
|
472
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
473
|
+
structuredContent: result
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
server.registerTool(
|
|
478
|
+
"normalize_bundle",
|
|
479
|
+
{
|
|
480
|
+
title: "Normalize Bundle",
|
|
481
|
+
description: "Plan an icon bundle by generating an AI-enhanced icon list from a description. Free but rate-limited. Review the list before generating.",
|
|
482
|
+
inputSchema: normalizeBundleSchema
|
|
483
|
+
},
|
|
484
|
+
async (args) => {
|
|
485
|
+
const result = await normalizeBundleTool(args);
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
488
|
+
structuredContent: result
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
server.registerTool(
|
|
493
|
+
"generate_bundle",
|
|
494
|
+
{
|
|
495
|
+
title: "Generate Bundle",
|
|
496
|
+
description: "Generate a bundle of icons from an icon list. Costs 1 credit per icon. Use normalize_bundle first to plan, or provide icons directly.",
|
|
497
|
+
inputSchema: generateBundleSchema
|
|
498
|
+
},
|
|
499
|
+
async (args) => {
|
|
500
|
+
const result = await generateBundleTool(args);
|
|
501
|
+
return {
|
|
502
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
503
|
+
structuredContent: result
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
return server;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/index.ts
|
|
511
|
+
async function main() {
|
|
512
|
+
const server = createServer();
|
|
513
|
+
const transport = new StdioServerTransport();
|
|
514
|
+
await server.connect(transport);
|
|
515
|
+
}
|
|
516
|
+
main().catch((error) => {
|
|
517
|
+
console.error("Fatal error:", error);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icogenie/mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for IcoGenie - Enable AI agents to generate icons programmatically",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"icogenie-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
19
|
+
"conf": "^12.0.0",
|
|
20
|
+
"open": "^10.0.0",
|
|
21
|
+
"zod": "^3.23.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"@repo/typescript-config": "0.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"icon",
|
|
36
|
+
"svg",
|
|
37
|
+
"generator",
|
|
38
|
+
"ai",
|
|
39
|
+
"icogenie",
|
|
40
|
+
"claude",
|
|
41
|
+
"anthropic"
|
|
42
|
+
],
|
|
43
|
+
"author": {
|
|
44
|
+
"name": "ICOGenie",
|
|
45
|
+
"url": "https://icogenie.com"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"homepage": "https://icogenie.com",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/icogenie/icogenie-mcp.git"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"registry": "https://registry.npmjs.org/"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup src/index.ts --format esm --target node18 --dts --clean",
|
|
59
|
+
"dev": "tsup src/index.ts --format esm --target node18 --watch",
|
|
60
|
+
"type-check": "tsc --noEmit",
|
|
61
|
+
"start": "node dist/index.js",
|
|
62
|
+
"version": "pnpm run build",
|
|
63
|
+
"postversion": "git push && git push --tags",
|
|
64
|
+
"release:patch": "npm version patch -m 'chore(icogenie-mcp): release v%s' --tag-version-prefix='mcp-v'",
|
|
65
|
+
"release:minor": "npm version minor -m 'chore(icogenie-mcp): release v%s' --tag-version-prefix='mcp-v'",
|
|
66
|
+
"release:major": "npm version major -m 'chore(icogenie-mcp): release v%s' --tag-version-prefix='mcp-v'"
|
|
67
|
+
}
|
|
68
|
+
}
|