spritecook-mcp 0.0.1 → 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 +69 -0
- package/bin/cli.mjs +8 -0
- package/package.json +19 -4
- package/src/auth.mjs +156 -0
- package/src/config.mjs +13 -0
- package/src/editors.mjs +214 -0
- package/src/setup.mjs +76 -0
- package/src/skill.mjs +73 -0
- package/src/ui.mjs +41 -0
- package/src/verify.mjs +41 -0
- package/index.js +0 -8
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# spritecook-mcp
|
|
2
|
+
|
|
3
|
+
Connect your AI agent (Cursor, VS Code, Claude Desktop, Claude Code) to [SpriteCook](https://spritecook.ai) for AI-powered pixel art and game asset generation.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx spritecook-mcp setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will:
|
|
12
|
+
|
|
13
|
+
1. **Authenticate** your SpriteCook account (browser-based or manual API key)
|
|
14
|
+
2. **Detect** your editors (Cursor, VS Code, Claude Desktop, Claude Code)
|
|
15
|
+
3. **Configure** MCP connections automatically
|
|
16
|
+
4. **Install** an optional agent skill for smarter AI integration
|
|
17
|
+
|
|
18
|
+
## What You Get
|
|
19
|
+
|
|
20
|
+
After setup, your AI agent can generate pixel art and game assets directly:
|
|
21
|
+
|
|
22
|
+
> "Generate a 64x64 pixel art sword sprite"
|
|
23
|
+
|
|
24
|
+
> "Create a character sprite sheet for my platformer game"
|
|
25
|
+
|
|
26
|
+
> "Make a set of potion icons with transparent backgrounds"
|
|
27
|
+
|
|
28
|
+
## Manual Configuration
|
|
29
|
+
|
|
30
|
+
If you prefer to configure manually, add this to your editor's MCP config:
|
|
31
|
+
|
|
32
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"spritecook": {
|
|
38
|
+
"url": "https://api.spritecook.ai/mcp/",
|
|
39
|
+
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**VS Code** (`.vscode/settings.json`):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcp": {
|
|
50
|
+
"servers": {
|
|
51
|
+
"spritecook": {
|
|
52
|
+
"type": "http",
|
|
53
|
+
"url": "https://api.spritecook.ai/mcp/",
|
|
54
|
+
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
- `SPRITECOOK_API_URL` - Override the API base URL (for local development)
|
|
64
|
+
|
|
65
|
+
## Links
|
|
66
|
+
|
|
67
|
+
- [SpriteCook](https://spritecook.ai) - AI game asset generator
|
|
68
|
+
- [API Documentation](https://spritecook.ai/docs) - Full API reference
|
|
69
|
+
- [Get API Key](https://app.spritecook.ai) - Sign up and manage API keys
|
package/bin/cli.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spritecook-mcp",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "SpriteCook MCP Server -
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SpriteCook MCP Server - Connect your AI agent (Cursor, VS Code, Claude) to SpriteCook for pixel art and game asset generation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spritecook",
|
|
7
7
|
"mcp",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"ai-agent",
|
|
12
12
|
"cursor",
|
|
13
13
|
"claude",
|
|
14
|
-
"vscode"
|
|
14
|
+
"vscode",
|
|
15
|
+
"cli"
|
|
15
16
|
],
|
|
16
17
|
"homepage": "https://spritecook.ai",
|
|
17
18
|
"repository": {
|
|
@@ -20,5 +21,19 @@
|
|
|
20
21
|
},
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"author": "SpriteCook <hello@spritecook.ai>",
|
|
23
|
-
"
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"spritecook-mcp": "./bin/cli.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"src/",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chalk": "^5.3.0",
|
|
35
|
+
"open": "^10.1.0",
|
|
36
|
+
"ora": "^8.1.0",
|
|
37
|
+
"prompts": "^2.4.2"
|
|
38
|
+
}
|
|
24
39
|
}
|
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { spinner, error, info } from './ui.mjs';
|
|
4
|
+
import { getApiBase } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Authenticate via the browser-based device authorization flow.
|
|
8
|
+
* Returns the raw API key string on success, or null on failure.
|
|
9
|
+
*/
|
|
10
|
+
export async function deviceFlow() {
|
|
11
|
+
const apiBase = getApiBase();
|
|
12
|
+
|
|
13
|
+
// Step 1: Start a device auth session
|
|
14
|
+
let session;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${apiBase}/v1/api/device-auth/start`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const body = await res.text();
|
|
22
|
+
error(`Failed to start device auth: ${res.status} ${body}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
session = await res.json();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
error(`Could not connect to SpriteCook API at ${apiBase}`);
|
|
28
|
+
error(err.message || String(err));
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { session_id, device_code, connect_url, expires_in } = session;
|
|
33
|
+
|
|
34
|
+
// Step 2: Open the browser
|
|
35
|
+
info(`Your verification code: ${device_code}`);
|
|
36
|
+
console.log();
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await open(connect_url);
|
|
40
|
+
info('Browser opened. Please log in and click "Authorize".');
|
|
41
|
+
} catch {
|
|
42
|
+
info(`Could not open browser automatically.`);
|
|
43
|
+
info(`Please open this URL manually:`);
|
|
44
|
+
console.log(` ${connect_url}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 3: Poll for completion
|
|
48
|
+
const spin = spinner('Waiting for authorization...');
|
|
49
|
+
spin.start();
|
|
50
|
+
|
|
51
|
+
const pollInterval = 3000; // 3 seconds
|
|
52
|
+
const deadline = Date.now() + expires_in * 1000;
|
|
53
|
+
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
await sleep(pollInterval);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${apiBase}/v1/api/device-auth/poll/${session_id}`);
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
continue; // Retry on transient errors
|
|
61
|
+
}
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
|
|
64
|
+
if (data.status === 'completed' && data.api_key) {
|
|
65
|
+
spin.succeed(' Authorized!');
|
|
66
|
+
return data.api_key;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (data.status === 'expired') {
|
|
70
|
+
spin.fail(' Session expired.');
|
|
71
|
+
error('The authorization session expired. Please try again.');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// status === 'pending' -- keep polling
|
|
76
|
+
} catch {
|
|
77
|
+
// Network hiccup, keep polling
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
spin.fail(' Timed out waiting for authorization.');
|
|
82
|
+
error('Authorization timed out. Please try again.');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Authenticate by having the user paste their API key manually.
|
|
88
|
+
* Returns the raw API key string on success, or null on failure.
|
|
89
|
+
*/
|
|
90
|
+
export async function manualKeyEntry() {
|
|
91
|
+
const response = await prompts({
|
|
92
|
+
type: 'text',
|
|
93
|
+
name: 'key',
|
|
94
|
+
message: 'Paste your SpriteCook API key (sc_live_...):',
|
|
95
|
+
validate: (val) => {
|
|
96
|
+
if (!val || !val.startsWith('sc_live_')) {
|
|
97
|
+
return 'API key must start with sc_live_';
|
|
98
|
+
}
|
|
99
|
+
if (val.length < 20) {
|
|
100
|
+
return 'API key seems too short';
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.key) {
|
|
107
|
+
return null; // User cancelled
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return response.key;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Prompt the user to choose an auth method and execute it.
|
|
115
|
+
* Returns the API key or null.
|
|
116
|
+
*/
|
|
117
|
+
export async function authenticate() {
|
|
118
|
+
const response = await prompts({
|
|
119
|
+
type: 'select',
|
|
120
|
+
name: 'method',
|
|
121
|
+
message: 'How would you like to authenticate?',
|
|
122
|
+
choices: [
|
|
123
|
+
{ title: 'Connect via browser (recommended)', value: 'browser' },
|
|
124
|
+
{ title: 'Enter API key manually', value: 'manual' },
|
|
125
|
+
],
|
|
126
|
+
initial: 0,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!response.method) {
|
|
130
|
+
return null; // User cancelled
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (response.method === 'browser') {
|
|
134
|
+
const key = await deviceFlow();
|
|
135
|
+
if (!key) {
|
|
136
|
+
info('Browser auth failed. You can try entering your key manually.');
|
|
137
|
+
const fallback = await prompts({
|
|
138
|
+
type: 'confirm',
|
|
139
|
+
name: 'retry',
|
|
140
|
+
message: 'Try manual key entry instead?',
|
|
141
|
+
initial: true,
|
|
142
|
+
});
|
|
143
|
+
if (fallback.retry) {
|
|
144
|
+
return manualKeyEntry();
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return key;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return manualKeyEntry();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sleep(ms) {
|
|
155
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
156
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration for the SpriteCook CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Get the API base URL (env override for local dev/testing) */
|
|
6
|
+
export function getApiBase() {
|
|
7
|
+
return process.env.SPRITECOOK_API_URL || 'https://api.spritecook.ai';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Get the MCP server URL */
|
|
11
|
+
export function getMcpUrl() {
|
|
12
|
+
return `${getApiBase()}/mcp/`;
|
|
13
|
+
}
|
package/src/editors.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { success, warn, info } from './ui.mjs';
|
|
5
|
+
import { getMcpUrl } from './config.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect which editors/IDEs are available and return a list of
|
|
9
|
+
* { name, detected, configPath, write(apiKey) } objects.
|
|
10
|
+
*/
|
|
11
|
+
export function detectEditors() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const home = homedir();
|
|
14
|
+
const editors = [];
|
|
15
|
+
|
|
16
|
+
// Cursor - project-level .cursor/mcp.json
|
|
17
|
+
const cursorDir = join(cwd, '.cursor');
|
|
18
|
+
editors.push({
|
|
19
|
+
name: 'Cursor',
|
|
20
|
+
detected: existsSync(cursorDir),
|
|
21
|
+
configPath: join(cursorDir, 'mcp.json'),
|
|
22
|
+
write: (apiKey) => writeCursorConfig(cursorDir, apiKey),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// VS Code - project-level .vscode/settings.json
|
|
26
|
+
const vscodeDir = join(cwd, '.vscode');
|
|
27
|
+
editors.push({
|
|
28
|
+
name: 'VS Code',
|
|
29
|
+
detected: existsSync(vscodeDir),
|
|
30
|
+
configPath: join(vscodeDir, 'settings.json'),
|
|
31
|
+
write: (apiKey) => writeVSCodeConfig(vscodeDir, apiKey),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Claude Desktop - platform-specific
|
|
35
|
+
const claudeDesktopPath = getClaudeDesktopConfigPath();
|
|
36
|
+
editors.push({
|
|
37
|
+
name: 'Claude Desktop',
|
|
38
|
+
detected: claudeDesktopPath ? existsSync(join(claudeDesktopPath, '..')) : false,
|
|
39
|
+
configPath: claudeDesktopPath,
|
|
40
|
+
write: (apiKey) => writeClaudeDesktopConfig(claudeDesktopPath, apiKey),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Claude Code - ~/.claude.json or ~/.claude/settings.json
|
|
44
|
+
const claudeCodePath1 = join(home, '.claude.json');
|
|
45
|
+
const claudeCodePath2 = join(home, '.claude', 'settings.json');
|
|
46
|
+
const claudeCodeDetected = existsSync(claudeCodePath1) || existsSync(claudeCodePath2);
|
|
47
|
+
const claudeCodeConfigPath = existsSync(claudeCodePath2) ? claudeCodePath2 : claudeCodePath1;
|
|
48
|
+
editors.push({
|
|
49
|
+
name: 'Claude Code',
|
|
50
|
+
detected: claudeCodeDetected,
|
|
51
|
+
configPath: claudeCodeConfigPath,
|
|
52
|
+
write: (apiKey) => writeClaudeCodeConfig(claudeCodeConfigPath, apiKey),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return editors;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Write MCP config for all detected editors.
|
|
60
|
+
* Returns the count of configs successfully written.
|
|
61
|
+
*/
|
|
62
|
+
export function writeConfigs(editors, apiKey) {
|
|
63
|
+
let written = 0;
|
|
64
|
+
|
|
65
|
+
for (const editor of editors) {
|
|
66
|
+
if (!editor.detected) continue;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
editor.write(apiKey);
|
|
70
|
+
success(`${editor.configPath}`);
|
|
71
|
+
written++;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
warn(`Failed to write ${editor.name} config: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return written;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Writer Functions ────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function writeCursorConfig(cursorDir, apiKey) {
|
|
83
|
+
const configPath = join(cursorDir, 'mcp.json');
|
|
84
|
+
const mcpUrl = getMcpUrl();
|
|
85
|
+
|
|
86
|
+
// Read existing config or start fresh
|
|
87
|
+
let config = {};
|
|
88
|
+
if (existsSync(configPath)) {
|
|
89
|
+
try {
|
|
90
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
// Corrupted file, start fresh
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Merge the spritecook server entry
|
|
97
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
98
|
+
config.mcpServers.spritecook = {
|
|
99
|
+
url: mcpUrl,
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${apiKey}`,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
ensureDir(cursorDir);
|
|
106
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeVSCodeConfig(vscodeDir, apiKey) {
|
|
110
|
+
const configPath = join(vscodeDir, 'settings.json');
|
|
111
|
+
const mcpUrl = getMcpUrl();
|
|
112
|
+
|
|
113
|
+
// Read existing config or start fresh
|
|
114
|
+
let config = {};
|
|
115
|
+
if (existsSync(configPath)) {
|
|
116
|
+
try {
|
|
117
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
118
|
+
} catch {
|
|
119
|
+
// Corrupted file, start fresh
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Merge into mcp.servers without overwriting other servers
|
|
124
|
+
if (!config.mcp) config.mcp = {};
|
|
125
|
+
if (!config.mcp.servers) config.mcp.servers = {};
|
|
126
|
+
config.mcp.servers.spritecook = {
|
|
127
|
+
type: 'http',
|
|
128
|
+
url: mcpUrl,
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Bearer ${apiKey}`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
ensureDir(vscodeDir);
|
|
135
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeClaudeDesktopConfig(configPath, apiKey) {
|
|
139
|
+
if (!configPath) {
|
|
140
|
+
warn('Claude Desktop config path not found for this platform.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const mcpUrl = getMcpUrl();
|
|
144
|
+
|
|
145
|
+
let config = {};
|
|
146
|
+
if (existsSync(configPath)) {
|
|
147
|
+
try {
|
|
148
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
149
|
+
} catch {
|
|
150
|
+
// Start fresh
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
155
|
+
config.mcpServers.spritecook = {
|
|
156
|
+
url: mcpUrl,
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${apiKey}`,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ensureDir(join(configPath, '..'));
|
|
163
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeClaudeCodeConfig(configPath, apiKey) {
|
|
167
|
+
const mcpUrl = getMcpUrl();
|
|
168
|
+
|
|
169
|
+
let config = {};
|
|
170
|
+
if (existsSync(configPath)) {
|
|
171
|
+
try {
|
|
172
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
173
|
+
} catch {
|
|
174
|
+
// Start fresh
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
179
|
+
config.mcpServers.spritecook = {
|
|
180
|
+
url: mcpUrl,
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Bearer ${apiKey}`,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
ensureDir(join(configPath, '..'));
|
|
187
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function getClaudeDesktopConfigPath() {
|
|
193
|
+
const platform = process.platform;
|
|
194
|
+
const home = homedir();
|
|
195
|
+
|
|
196
|
+
if (platform === 'darwin') {
|
|
197
|
+
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
198
|
+
}
|
|
199
|
+
if (platform === 'win32') {
|
|
200
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
201
|
+
return join(appData, 'Claude', 'claude_desktop_config.json');
|
|
202
|
+
}
|
|
203
|
+
if (platform === 'linux') {
|
|
204
|
+
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ensureDir(dir) {
|
|
211
|
+
if (!existsSync(dir)) {
|
|
212
|
+
mkdirSync(dir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { printBanner, step, success, info, warn, error } from './ui.mjs';
|
|
3
|
+
import { authenticate } from './auth.mjs';
|
|
4
|
+
import { verifyApiKey } from './verify.mjs';
|
|
5
|
+
import { detectEditors, writeConfigs } from './editors.mjs';
|
|
6
|
+
import { maybeInstallSkill } from './skill.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main setup flow orchestrator.
|
|
10
|
+
* Called by bin/cli.mjs as the entry point.
|
|
11
|
+
*/
|
|
12
|
+
export async function run() {
|
|
13
|
+
printBanner();
|
|
14
|
+
|
|
15
|
+
// ── Step 1: Authenticate ──────────────────────────────────────────
|
|
16
|
+
step(1, 'Authentication');
|
|
17
|
+
|
|
18
|
+
const apiKey = await authenticate();
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
console.log();
|
|
21
|
+
error('Setup cancelled. No API key obtained.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Step 2: Verify the key works ──────────────────────────────────
|
|
26
|
+
step(2, 'Verification');
|
|
27
|
+
|
|
28
|
+
const verification = await verifyApiKey(apiKey);
|
|
29
|
+
if (!verification.ok) {
|
|
30
|
+
error('API key verification failed. Please check your key and try again.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Step 3: Detect editors and write configs ──────────────────────
|
|
35
|
+
step(3, 'Editor Configuration');
|
|
36
|
+
|
|
37
|
+
const editors = detectEditors();
|
|
38
|
+
const detected = editors.filter((e) => e.detected);
|
|
39
|
+
|
|
40
|
+
if (detected.length === 0) {
|
|
41
|
+
warn('No supported editors detected in the current directory.');
|
|
42
|
+
info('Supported editors: Cursor, VS Code, Claude Desktop, Claude Code');
|
|
43
|
+
info('Run this command from your project root where .cursor/ or .vscode/ exists.');
|
|
44
|
+
console.log();
|
|
45
|
+
info('You can also configure manually. Your API key:');
|
|
46
|
+
console.log(` ${chalk.dim(apiKey.slice(0, 16) + '...')}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.log();
|
|
49
|
+
info('Detected editors:');
|
|
50
|
+
for (const editor of editors) {
|
|
51
|
+
const mark = editor.detected ? chalk.green('[x]') : chalk.dim('[ ]');
|
|
52
|
+
const label = editor.detected ? editor.name : chalk.dim(`${editor.name} (not found)`);
|
|
53
|
+
console.log(` ${mark} ${label}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log();
|
|
57
|
+
info('Writing MCP config...');
|
|
58
|
+
const written = writeConfigs(detected, apiKey);
|
|
59
|
+
|
|
60
|
+
if (written === 0) {
|
|
61
|
+
warn('No configs were written. You may need to configure manually.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Step 4: Optional agent skill ─────────────────────────────────
|
|
66
|
+
step(4, 'Agent Skill (optional)');
|
|
67
|
+
await maybeInstallSkill();
|
|
68
|
+
|
|
69
|
+
// ── Done ──────────────────────────────────────────────────────────
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.bold.green(' Setup complete!'));
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(chalk.dim(' Try asking your AI agent:'));
|
|
74
|
+
console.log(chalk.white(' "Generate a 64x64 pixel art sword sprite"'));
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
package/src/skill.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { success, info } from './ui.mjs';
|
|
5
|
+
|
|
6
|
+
const SKILL_CONTENT = `# SpriteCook - AI Game Asset Generator
|
|
7
|
+
|
|
8
|
+
Use SpriteCook MCP tools when the user needs pixel art sprites, game assets, icons, or visual game content.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- User asks to "generate a sprite", "create pixel art", "make a game asset", "create an icon"
|
|
13
|
+
- User needs visual assets for a game project (characters, items, backgrounds, UI elements)
|
|
14
|
+
- User references game art, sprites, or pixel art in their request
|
|
15
|
+
|
|
16
|
+
## Available MCP Tools
|
|
17
|
+
|
|
18
|
+
### generate_pixel_art
|
|
19
|
+
Generate pixel art or detailed game assets from a text prompt.
|
|
20
|
+
|
|
21
|
+
**Parameters:**
|
|
22
|
+
- \`prompt\` (required): Description of what to generate (e.g. "a medieval knight with a sword")
|
|
23
|
+
- \`pixel\` (optional, default true): Whether to generate pixel art style
|
|
24
|
+
- \`size\` (optional, default 64): Sprite size in pixels (16, 32, 64, 128, 256, 512)
|
|
25
|
+
- \`background\` (optional, default "transparent"): Background mode ("transparent", "white", "include")
|
|
26
|
+
|
|
27
|
+
### check_job_status
|
|
28
|
+
Check the status of a generation job. Returns progress and result URLs when complete.
|
|
29
|
+
|
|
30
|
+
### get_credit_balance
|
|
31
|
+
Check remaining credits on the connected SpriteCook account.
|
|
32
|
+
|
|
33
|
+
## Tips
|
|
34
|
+
|
|
35
|
+
- For game characters: use size 64 or 128 with transparent background
|
|
36
|
+
- For icons/items: use size 32 or 64 with transparent background
|
|
37
|
+
- For tilesets/backgrounds: use size 128 or 256 with "include" background
|
|
38
|
+
- Be specific in prompts: "a red dragon breathing fire, side view" works better than "dragon"
|
|
39
|
+
- The tool returns download URLs for the generated images
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optionally install the SpriteCook agent skill for Cursor.
|
|
44
|
+
* Only offered if .cursor/ directory exists.
|
|
45
|
+
*/
|
|
46
|
+
export async function maybeInstallSkill() {
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const cursorDir = join(cwd, '.cursor');
|
|
49
|
+
|
|
50
|
+
if (!existsSync(cursorDir)) {
|
|
51
|
+
return; // Cursor not detected, skip silently
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const response = await prompts({
|
|
55
|
+
type: 'confirm',
|
|
56
|
+
name: 'install',
|
|
57
|
+
message: 'Install SpriteCook agent skill for Cursor? (helps AI use SpriteCook proactively)',
|
|
58
|
+
initial: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.install) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const skillDir = join(cursorDir, 'skills', 'spritecook');
|
|
66
|
+
if (!existsSync(skillDir)) {
|
|
67
|
+
mkdirSync(skillDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
71
|
+
writeFileSync(skillPath, SKILL_CONTENT, 'utf-8');
|
|
72
|
+
success(`Agent skill installed at .cursor/skills/spritecook/SKILL.md`);
|
|
73
|
+
}
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
/** Print the SpriteCook header banner */
|
|
5
|
+
export function printBanner() {
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(chalk.bold(' SpriteCook MCP Setup'));
|
|
8
|
+
console.log(chalk.dim(' Connect your AI agent to SpriteCook'));
|
|
9
|
+
console.log();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Print a success message */
|
|
13
|
+
export function success(msg) {
|
|
14
|
+
console.log(chalk.green(' [ok]') + ' ' + msg);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Print an info message */
|
|
18
|
+
export function info(msg) {
|
|
19
|
+
console.log(chalk.blue(' [info]') + ' ' + msg);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Print a warning message */
|
|
23
|
+
export function warn(msg) {
|
|
24
|
+
console.log(chalk.yellow(' [warn]') + ' ' + msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Print an error message */
|
|
28
|
+
export function error(msg) {
|
|
29
|
+
console.log(chalk.red(' [error]') + ' ' + msg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Create a spinner */
|
|
33
|
+
export function spinner(text) {
|
|
34
|
+
return ora({ text: ' ' + text, color: 'cyan' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Print a step header */
|
|
38
|
+
export function step(num, msg) {
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.bold.white(` ${num}. ${msg}`));
|
|
41
|
+
}
|
package/src/verify.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { spinner, success, error } from './ui.mjs';
|
|
2
|
+
import { getApiBase } from './config.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify an API key by calling the credits endpoint.
|
|
6
|
+
* Returns { ok: boolean, credits?: number, tier?: string }
|
|
7
|
+
*/
|
|
8
|
+
export async function verifyApiKey(apiKey) {
|
|
9
|
+
const spin = spinner('Verifying API key...');
|
|
10
|
+
spin.start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${getApiBase()}/v1/api/credits`, {
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${apiKey}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
spin.fail(' Verification failed');
|
|
21
|
+
const body = await res.text();
|
|
22
|
+
if (res.status === 401) {
|
|
23
|
+
error('Invalid or expired API key.');
|
|
24
|
+
} else {
|
|
25
|
+
error(`API returned ${res.status}: ${body}`);
|
|
26
|
+
}
|
|
27
|
+
return { ok: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
const credits = data.credits ?? data.credits_remaining ?? 0;
|
|
32
|
+
const tier = data.tier || data.subscription_tier || 'free';
|
|
33
|
+
|
|
34
|
+
spin.succeed(` Verified! (${credits} credits, ${tier} tier)`);
|
|
35
|
+
return { ok: true, credits, tier };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spin.fail(' Verification failed');
|
|
38
|
+
error(`Could not reach SpriteCook API: ${err.message || err}`);
|
|
39
|
+
return { ok: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
package/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// SpriteCook MCP Server - Model Context Protocol server for AI-powered pixel art generation
|
|
2
|
-
// This package is a placeholder. Full MCP server package coming soon.
|
|
3
|
-
// Visit https://spritecook.ai for more information.
|
|
4
|
-
|
|
5
|
-
module.exports = {
|
|
6
|
-
version: "0.0.1",
|
|
7
|
-
message: "SpriteCook MCP Server coming soon. Visit https://spritecook.ai for more information.",
|
|
8
|
-
};
|