stackapps-suite-mcp 0.1.5__py3-none-any.whl
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.
- stackapps_mcp/__init__.py +0 -0
- stackapps_mcp/__main__.py +4 -0
- stackapps_mcp/blueprint.txt +337 -0
- stackapps_mcp/http_main.py +20 -0
- stackapps_mcp/imagcon.py +135 -0
- stackapps_mcp/main.py +38 -0
- stackapps_mcp/server.py +223 -0
- stackapps_mcp/x402_http.py +104 -0
- stackapps_suite_mcp-0.1.5.dist-info/METADATA +93 -0
- stackapps_suite_mcp-0.1.5.dist-info/RECORD +12 -0
- stackapps_suite_mcp-0.1.5.dist-info/WHEEL +4 -0
- stackapps_suite_mcp-0.1.5.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# BLUEPRINT: StackApps MCP Gateway [x402]
|
|
2
|
+
# Version: 1.3.0
|
|
3
|
+
# URL: https://mcp.stackapps.app
|
|
4
|
+
# Updated: 2026-07-04
|
|
5
|
+
|
|
6
|
+
## IDENTITY
|
|
7
|
+
name: StackApps MCP Gateway
|
|
8
|
+
description: Unified x402 gateway for the StackApps tool suite. Pay per call in USDC
|
|
9
|
+
on Base mainnet — no account required to start; first x402 payment may issue a temp
|
|
10
|
+
API key (first payment only). Each member app (Imagcon today; StackBill, StackSlip,
|
|
11
|
+
and others as they ship) contributes tools here. One server, one install, the full suite.
|
|
12
|
+
category: developer-tools
|
|
13
|
+
contact: support@stackapps.app
|
|
14
|
+
|
|
15
|
+
## SUMMARY
|
|
16
|
+
tagline: One server. All StackApps tools. Pay per call in USDC.
|
|
17
|
+
audience: AI agents and developers who need StackApps suite tools without creating an
|
|
18
|
+
account. Requires an EVM wallet funded with USDC on Base mainnet (eip155:8453).
|
|
19
|
+
capabilities:
|
|
20
|
+
- create-pwa-icons-from-image: Upload image → full PWA icon set (27 files + manifest.json) — $0.10 USDC
|
|
21
|
+
- create-splash-screens-from-image: Upload image → 16 iOS/iPad splash screens — $0.10 USDC
|
|
22
|
+
- generate-pwa-icons: Text prompt → AI-generated PWA icon set — $0.295 USDC
|
|
23
|
+
- generate-image: Text prompt → AI-generated source image (image_key + preview URL) — $0.195 USDC
|
|
24
|
+
- generate-splash-screens: Text prompt → AI-generated 16 iOS/iPad splash screens — $0.295 USDC
|
|
25
|
+
- get-wallet-profile: Look up temp vs permanent wallet profile — free
|
|
26
|
+
- setup-wallet-profile: Activate permanent account from first-payment profile token — free
|
|
27
|
+
|
|
28
|
+
## AUTH
|
|
29
|
+
provider: none
|
|
30
|
+
payment: x402 — EVM wallet, USDC on Base mainnet (eip155:8453)
|
|
31
|
+
note: No Firebase account or API key required to start. Each tool call settles independently
|
|
32
|
+
on-chain via EIP-3009. The MCP server signs payment authorizations with the
|
|
33
|
+
configured wallet and retries automatically — no user interaction needed per call.
|
|
34
|
+
|
|
35
|
+
## WALLET PROFILE
|
|
36
|
+
note: On every first payment from a new wallet, Imagcon auto-creates a 60-day
|
|
37
|
+
temporary account linked to that wallet address. No action required.
|
|
38
|
+
headers-on-first-payment:
|
|
39
|
+
- X-Imagcon-Token: profile token (tok_...) — stored in session only, never echoed in tool text
|
|
40
|
+
- X-Imagcon-Profile-Setup: activate URL (non-secret) — first payment only
|
|
41
|
+
- X-Imagcon-Api-Key: Imagcon API key — first payment only; unlocks all 14 imagcon-mcp tools
|
|
42
|
+
surfaced-by: stackapps-suite-mcp appends a non-secret opt-in message to tool results on
|
|
43
|
+
first payment only (when X-Imagcon-Profile-Setup is present). Secrets stay in session
|
|
44
|
+
memory — never in chat logs. Example: "A 60-day temp account was created for your wallet.
|
|
45
|
+
To keep it permanently (free), call setup_wallet_profile with the profile token from
|
|
46
|
+
secure storage, or visit the activate URL. An Imagcon API key was issued — use imagcon-mcp
|
|
47
|
+
with that key (store securely, not in chat)."
|
|
48
|
+
activate-endpoint: POST https://imagcon.app/routes/x402/profile/activate
|
|
49
|
+
body: { "profile_token": "tok_...", "wallet_address": "0x...", "name": "...",
|
|
50
|
+
"terms_confirmed": true, "signature": "0x...", "message": "imagcon.app/profile/activate:tok_...:<unix_ts>" }
|
|
51
|
+
signature: EIP-191 personal_sign over the message, signed with the paying wallet
|
|
52
|
+
key — stackapps-suite-mcp signs automatically; no agent input required.
|
|
53
|
+
Server verifies the recovered address matches wallet_address (±300s window).
|
|
54
|
+
terms: https://imagcon.app/terms-of-service
|
|
55
|
+
note: stackapps-suite-mcp derives wallet_address from WALLET_PRIVATE_KEY automatically;
|
|
56
|
+
agents pass name and terms_confirmed to setup_wallet_profile. profile_token defaults
|
|
57
|
+
to the session token from the first paid call if omitted.
|
|
58
|
+
paper-plan: If setup_wallet_profile is never called, the temp account auto-deletes
|
|
59
|
+
after 60 days. Next payment from same wallet starts a fresh 60-day window.
|
|
60
|
+
|
|
61
|
+
## MCP
|
|
62
|
+
server: stackapps-suite-mcp
|
|
63
|
+
preferred-transport: stdio
|
|
64
|
+
http-transport: discovery only — https://mcp.stackapps.app serves blueprint.txt; paid tools
|
|
65
|
+
require local stdio with WALLET_PRIVATE_KEY. Do not use HTTP MCP on Cloud Run.
|
|
66
|
+
install: claude mcp add stackapps-suite -- uvx stackapps-suite-mcp --wallet-key 0x...
|
|
67
|
+
package: stackapps-suite-mcp on PyPI
|
|
68
|
+
fallback-wheel: https://stackapps.app/downloads/stackapps_suite_mcp-0.1.5-py3-none-any.whl
|
|
69
|
+
requirements: uv/uvx installed (https://docs.astral.sh/uv/). Python 3.11–3.12.
|
|
70
|
+
EVM wallet private key. USDC on Base mainnet in that wallet.
|
|
71
|
+
|
|
72
|
+
### TRANSPORT (stdio)
|
|
73
|
+
command: uvx stackapps-suite-mcp --wallet-key 0x...
|
|
74
|
+
note: Runs as a local stdio process. Use WALLET_PRIVATE_KEY env var rather than
|
|
75
|
+
passing the key on the command line.
|
|
76
|
+
|
|
77
|
+
### MCP TOOLS
|
|
78
|
+
server: stackapps-suite-mcp
|
|
79
|
+
|
|
80
|
+
create_pwa_icons_from_image | image_path, output_dir | Full PWA icon set (27 files + manifest.json) from PNG/JPEG/WebP ($0.10 USDC) | source: imagcon.app
|
|
81
|
+
create_splash_screens_from_image | image_path, background_color, output_dir | 16 iOS/iPad splash screens from PNG/JPEG/WebP ($0.10 USDC) | source: imagcon.app
|
|
82
|
+
generate_pwa_icons | description, output_dir | AI-generate PWA icon set from text prompt ($0.295 USDC) | source: imagcon.app
|
|
83
|
+
generate_image | description | AI-generate source image, returns image_key + preview_url ($0.195 USDC) | source: imagcon.app
|
|
84
|
+
generate_splash_screens | description, background_color, output_dir | AI-generate 16 iOS/iPad splash screens ($0.295 USDC) | source: imagcon.app
|
|
85
|
+
get_wallet_profile | (none) | Look up temp vs permanent wallet profile (free) | source: imagcon.app
|
|
86
|
+
setup_wallet_profile | name, terms_confirmed, profile_token, company_name, tax_id | Activate permanent account from first-payment profile token (free) | source: imagcon.app
|
|
87
|
+
|
|
88
|
+
### PAYMENTS
|
|
89
|
+
network: Base mainnet (eip155:8453)
|
|
90
|
+
asset: USDC
|
|
91
|
+
mechanism: x402 (HTTP 402 / EIP-3009)
|
|
92
|
+
flow: Each tool call makes an HTTP request to the source app's x402 endpoint. The
|
|
93
|
+
server receives a 402 challenge, signs a payment authorization with the configured
|
|
94
|
+
wallet, and retries. Settlement is on-chain; funds go directly to the source app's
|
|
95
|
+
wallet. StackApps takes no cut.
|
|
96
|
+
prices:
|
|
97
|
+
- $0.10 USDC: create_pwa_icons_from_image, create_splash_screens_from_image
|
|
98
|
+
- $0.195 USDC: generate_image
|
|
99
|
+
- $0.295 USDC: generate_pwa_icons, generate_splash_screens
|
|
100
|
+
- free: get_wallet_profile, setup_wallet_profile (no x402 charge)
|
|
101
|
+
client-safety: The server refuses to sign any payment challenge that does not pay
|
|
102
|
+
the StackApps suite wallet, or that exceeds the per-call cap ($0.50 USDC by
|
|
103
|
+
default; override with STACKAPPS_MAX_USD_PER_CALL). A compromised or spoofed
|
|
104
|
+
endpoint cannot redirect or inflate payments.
|
|
105
|
+
|
|
106
|
+
### REQUIRED-SECRETS
|
|
107
|
+
- WALLET_PRIVATE_KEY:
|
|
108
|
+
description: EVM wallet private key for signing x402 payment authorizations
|
|
109
|
+
format: 0x-prefixed hex string (64 hex chars after 0x)
|
|
110
|
+
funding: Wallet must hold USDC on Base mainnet.
|
|
111
|
+
storage:
|
|
112
|
+
shell: export WALLET_PRIVATE_KEY=0x... in ~/.zshrc or ~/.bashrc
|
|
113
|
+
claude-code: pass via --wallet-key argument in claude mcp add command
|
|
114
|
+
warning: never commit the raw key to git; never set on Cloud Run
|
|
115
|
+
- IMAGCON_PROFILE_TOKEN:
|
|
116
|
+
description: Optional override for X-Imagcon-Token header on subsequent x402 calls
|
|
117
|
+
format: tok_... string from Imagcon first-payment response
|
|
118
|
+
note: Normally stored in session memory automatically after first paid call
|
|
119
|
+
|
|
120
|
+
### CLIENT-CONFIGS
|
|
121
|
+
|
|
122
|
+
#### Claude Code (CLI)
|
|
123
|
+
claude mcp add stackapps-suite -- uvx stackapps-suite-mcp --wallet-key 0x...
|
|
124
|
+
|
|
125
|
+
#### Claude Code / Cursor / Windsurf / Cline (JSON)
|
|
126
|
+
{
|
|
127
|
+
"mcpServers": {
|
|
128
|
+
"stackapps-suite": {
|
|
129
|
+
"command": "uvx",
|
|
130
|
+
"args": ["stackapps-suite-mcp", "--wallet-key", "0x..."]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
## ACCESS
|
|
136
|
+
preferred: mcp
|
|
137
|
+
|
|
138
|
+
## CAPABILITY: create-pwa-icons-from-image
|
|
139
|
+
description: Generate a full PWA icon set from an existing local image file.
|
|
140
|
+
Produces 27 files covering all required PWA, iOS, Android, Windows tile, and
|
|
141
|
+
Safari pinned-tab sizes, plus manifest.json, packaged as a ZIP and extracted
|
|
142
|
+
to output_dir. No AI generation — source image is uploaded directly.
|
|
143
|
+
Powered by Imagcon (imagcon.app).
|
|
144
|
+
input:
|
|
145
|
+
- name: image_path
|
|
146
|
+
type: string
|
|
147
|
+
required: true
|
|
148
|
+
description: Absolute path to a PNG, JPEG, or WebP file on the local machine.
|
|
149
|
+
- name: output_dir
|
|
150
|
+
type: string
|
|
151
|
+
required: false
|
|
152
|
+
default: ./public/icons
|
|
153
|
+
description: Directory where the ZIP is extracted. Relative paths resolve
|
|
154
|
+
against the MCP server process directory.
|
|
155
|
+
output:
|
|
156
|
+
- type: file
|
|
157
|
+
description: ZIP extracted to output_dir. Contains icons/ subdirectory and manifest.json.
|
|
158
|
+
auth-required: false
|
|
159
|
+
payment: $0.10 USDC per call (Base mainnet, x402)
|
|
160
|
+
source: imagcon.app/routes/x402/icons-from-image
|
|
161
|
+
|
|
162
|
+
### MCP
|
|
163
|
+
tool: create_pwa_icons_from_image
|
|
164
|
+
|
|
165
|
+
## CAPABILITY: create-splash-screens-from-image
|
|
166
|
+
description: Generate 16 iOS and iPad launch splash screens from an existing local
|
|
167
|
+
image file. Covers all current iPhone and iPad canvas sizes. Output is a ZIP
|
|
168
|
+
of flat PNG files extracted to output_dir. No AI generation — source image is
|
|
169
|
+
uploaded directly. Powered by Imagcon (imagcon.app).
|
|
170
|
+
input:
|
|
171
|
+
- name: image_path
|
|
172
|
+
type: string
|
|
173
|
+
required: true
|
|
174
|
+
description: Absolute path to a PNG, JPEG, or WebP file on the local machine.
|
|
175
|
+
- name: background_color
|
|
176
|
+
type: string
|
|
177
|
+
required: false
|
|
178
|
+
default: "#ffffff"
|
|
179
|
+
description: Hex color string for the splash screen background (e.g. #1a1a2e).
|
|
180
|
+
- name: output_dir
|
|
181
|
+
type: string
|
|
182
|
+
required: false
|
|
183
|
+
default: ./public/splash
|
|
184
|
+
description: Directory where the ZIP is extracted.
|
|
185
|
+
output:
|
|
186
|
+
- type: file
|
|
187
|
+
description: ZIP extracted to output_dir. Contains 16 flat PNG splash screens.
|
|
188
|
+
auth-required: false
|
|
189
|
+
payment: $0.10 USDC per call (Base mainnet, x402)
|
|
190
|
+
source: imagcon.app/routes/x402/splash-from-image
|
|
191
|
+
|
|
192
|
+
### MCP
|
|
193
|
+
tool: create_splash_screens_from_image
|
|
194
|
+
|
|
195
|
+
## CAPABILITY: generate-pwa-icons
|
|
196
|
+
description: AI-generate a complete PWA icon set from a text description of your app.
|
|
197
|
+
Imagcon generates a source image from the prompt, then produces all 27 PWA icon
|
|
198
|
+
sizes plus manifest.json, packaged as a ZIP extracted to output_dir.
|
|
199
|
+
Powered by Imagcon (imagcon.app).
|
|
200
|
+
input:
|
|
201
|
+
- name: description
|
|
202
|
+
type: string
|
|
203
|
+
required: true
|
|
204
|
+
description: Text description of the app — used as the AI image generation prompt.
|
|
205
|
+
Be specific about colors, style, and subject matter.
|
|
206
|
+
- name: output_dir
|
|
207
|
+
type: string
|
|
208
|
+
required: false
|
|
209
|
+
default: ./public/icons
|
|
210
|
+
description: Directory where the ZIP is extracted.
|
|
211
|
+
output:
|
|
212
|
+
- type: file
|
|
213
|
+
description: ZIP extracted to output_dir. Contains AI-generated source image,
|
|
214
|
+
icons/ subdirectory, and manifest.json.
|
|
215
|
+
auth-required: false
|
|
216
|
+
payment: $0.295 USDC per call (Base mainnet, x402)
|
|
217
|
+
source: imagcon.app/routes/x402/generate-pwa-icons
|
|
218
|
+
|
|
219
|
+
### MCP
|
|
220
|
+
tool: generate_pwa_icons
|
|
221
|
+
|
|
222
|
+
## CAPABILITY: generate-image
|
|
223
|
+
description: AI-generate a source image from a text description. Returns image_key
|
|
224
|
+
and a preview URL. Use this when you want to review the generated image before
|
|
225
|
+
committing to a full icon set generation. Powered by Imagcon (imagcon.app).
|
|
226
|
+
input:
|
|
227
|
+
- name: description
|
|
228
|
+
type: string
|
|
229
|
+
required: true
|
|
230
|
+
description: Text description used as the AI image generation prompt.
|
|
231
|
+
output:
|
|
232
|
+
- type: json
|
|
233
|
+
description: image_key (string) and preview_url (string). The image_key can
|
|
234
|
+
be passed to Imagcon's create_icons_from_key tool (via imagcon-mcp) to
|
|
235
|
+
generate the full icon set without regenerating the image.
|
|
236
|
+
auth-required: false
|
|
237
|
+
payment: $0.195 USDC per call (Base mainnet, x402)
|
|
238
|
+
source: imagcon.app/routes/x402/generate-image
|
|
239
|
+
|
|
240
|
+
### MCP
|
|
241
|
+
tool: generate_image
|
|
242
|
+
|
|
243
|
+
## CAPABILITY: generate-splash-screens
|
|
244
|
+
description: AI-generate 16 iOS and iPad launch splash screens from a text description
|
|
245
|
+
of your app. Imagcon generates a source image from the prompt, then produces all
|
|
246
|
+
splash screen canvas sizes, packaged as a ZIP extracted to output_dir.
|
|
247
|
+
Powered by Imagcon (imagcon.app).
|
|
248
|
+
input:
|
|
249
|
+
- name: description
|
|
250
|
+
type: string
|
|
251
|
+
required: true
|
|
252
|
+
description: Text description used as the AI image generation prompt.
|
|
253
|
+
- name: background_color
|
|
254
|
+
type: string
|
|
255
|
+
required: false
|
|
256
|
+
default: "#ffffff"
|
|
257
|
+
description: Hex color string for the splash screen background.
|
|
258
|
+
- name: output_dir
|
|
259
|
+
type: string
|
|
260
|
+
required: false
|
|
261
|
+
default: ./public/splash
|
|
262
|
+
description: Directory where the ZIP is extracted.
|
|
263
|
+
output:
|
|
264
|
+
- type: file
|
|
265
|
+
description: ZIP extracted to output_dir. Contains AI-generated source image
|
|
266
|
+
and 16 flat PNG splash screens.
|
|
267
|
+
auth-required: false
|
|
268
|
+
payment: $0.295 USDC per call (Base mainnet, x402)
|
|
269
|
+
source: imagcon.app/routes/x402/generate-splash-screens
|
|
270
|
+
|
|
271
|
+
### MCP
|
|
272
|
+
tool: generate_splash_screens
|
|
273
|
+
|
|
274
|
+
## CAPABILITY: get-wallet-profile
|
|
275
|
+
description: Look up the Imagcon wallet profile for the configured wallet address.
|
|
276
|
+
Use before prompting the user to activate — shows temp vs permanent status.
|
|
277
|
+
input: []
|
|
278
|
+
output:
|
|
279
|
+
- type: string
|
|
280
|
+
description: Profile status and optional display name.
|
|
281
|
+
auth-required: false
|
|
282
|
+
payment: free (no x402 charge)
|
|
283
|
+
source: imagcon.app/routes/x402/profile/{wallet}
|
|
284
|
+
|
|
285
|
+
### MCP
|
|
286
|
+
tool: get_wallet_profile
|
|
287
|
+
|
|
288
|
+
## CAPABILITY: setup-wallet-profile
|
|
289
|
+
description: Activate a permanent Imagcon account linked to your wallet. Call this
|
|
290
|
+
after the first x402 payment. profile_token is stored in session memory after the
|
|
291
|
+
first paid call; pass explicitly from secure storage if needed. Permanent accounts
|
|
292
|
+
keep payment history indefinitely. Without activation the temp account auto-deletes
|
|
293
|
+
after 60 days (paper plan).
|
|
294
|
+
input:
|
|
295
|
+
- name: name
|
|
296
|
+
type: string
|
|
297
|
+
required: true
|
|
298
|
+
description: Display name for the profile (person or business name).
|
|
299
|
+
- name: terms_confirmed
|
|
300
|
+
type: boolean
|
|
301
|
+
required: true
|
|
302
|
+
description: Must be true only after the human has read https://imagcon.app/terms-of-service and agreed.
|
|
303
|
+
- name: profile_token
|
|
304
|
+
type: string
|
|
305
|
+
required: false
|
|
306
|
+
description: tok_... token from first x402 payment. Defaults to session token if omitted.
|
|
307
|
+
- name: wallet_address
|
|
308
|
+
type: string
|
|
309
|
+
required: true
|
|
310
|
+
description: Derived from WALLET_PRIVATE_KEY by stackapps-suite-mcp (not an agent input).
|
|
311
|
+
- name: company_name
|
|
312
|
+
type: string
|
|
313
|
+
required: false
|
|
314
|
+
description: Optional business name.
|
|
315
|
+
- name: tax_id
|
|
316
|
+
type: string
|
|
317
|
+
required: false
|
|
318
|
+
description: Optional tax ID / VAT number.
|
|
319
|
+
output:
|
|
320
|
+
- type: string
|
|
321
|
+
description: Confirmation message with account status.
|
|
322
|
+
auth-required: false
|
|
323
|
+
payment: free (no x402 charge)
|
|
324
|
+
source: imagcon.app/routes/x402/profile/activate
|
|
325
|
+
|
|
326
|
+
### MCP
|
|
327
|
+
tool: setup_wallet_profile
|
|
328
|
+
|
|
329
|
+
## SUITE
|
|
330
|
+
note: This gateway grows as member apps ship x402 endpoints. Each new capability
|
|
331
|
+
will appear in ## MCP TOOLS above and as a ## CAPABILITY block below.
|
|
332
|
+
The install command and server URL do not change — agents with stackapps-suite-mcp
|
|
333
|
+
already installed receive new tools automatically on next connect.
|
|
334
|
+
members:
|
|
335
|
+
- imagcon.app — PWA icons, splash screens, AI image generation (live)
|
|
336
|
+
- stackbill.app — invoicing (planned)
|
|
337
|
+
- stackslip.app — packing slips (planned)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
from stackapps_mcp.server import create_app
|
|
8
|
+
|
|
9
|
+
# Cloud Run serves blueprint discovery only. NEVER set WALLET_PRIVATE_KEY here —
|
|
10
|
+
# a funded wallet on a public endpoint would be a drain vector.
|
|
11
|
+
app = create_app(discovery_only=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
port = int(os.environ.get("PORT", "8080"))
|
|
16
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
stackapps_mcp/imagcon.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from stackapps_mcp.x402_http import SuiteX402Http
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ImagconX402Client:
|
|
13
|
+
"""Imagcon member-app adapter: endpoint URLs + profile-token handling.
|
|
14
|
+
|
|
15
|
+
Payment signing, retry, and safety guards live in SuiteX402Http. New member
|
|
16
|
+
apps follow this same shape — see JOINING-THE-GATEWAY.md.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
ICONS_URL = "https://imagcon.app/routes/x402/icons-from-image"
|
|
20
|
+
SPLASH_URL = "https://imagcon.app/routes/x402/splash-from-image"
|
|
21
|
+
GENERATE_IMAGE_URL = "https://imagcon.app/routes/x402/generate-image"
|
|
22
|
+
GENERATE_ICONS_AI_URL = "https://imagcon.app/routes/x402/generate-pwa-icons"
|
|
23
|
+
GENERATE_SPLASH_AI_URL = "https://imagcon.app/routes/x402/generate-splash-screens"
|
|
24
|
+
PROFILE_ACTIVATE_URL = "https://imagcon.app/routes/x402/profile/activate"
|
|
25
|
+
PROFILE_LOOKUP_URL = "https://imagcon.app/routes/x402/profile"
|
|
26
|
+
|
|
27
|
+
def __init__(self, private_key: str, network: str = "eip155:8453") -> None:
|
|
28
|
+
self._x402 = SuiteX402Http(private_key, network)
|
|
29
|
+
env_token = (os.environ.get("IMAGCON_PROFILE_TOKEN") or "").strip()
|
|
30
|
+
self._profile_token: str | None = env_token or None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def wallet_address(self) -> str:
|
|
34
|
+
return self._x402.wallet_address
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def profile_token(self) -> str | None:
|
|
38
|
+
return self._profile_token
|
|
39
|
+
|
|
40
|
+
def _profile_headers(self) -> dict[str, str]:
|
|
41
|
+
if self._profile_token:
|
|
42
|
+
return {"X-Imagcon-Token": self._profile_token}
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
def _extract_profile(self, response: httpx.Response) -> dict:
|
|
46
|
+
extra: dict = {}
|
|
47
|
+
if token := response.headers.get("x-imagcon-token"):
|
|
48
|
+
extra["profile_token"] = token
|
|
49
|
+
self._profile_token = token
|
|
50
|
+
if setup_url := response.headers.get("x-imagcon-profile-setup"):
|
|
51
|
+
extra["is_first_payment"] = True
|
|
52
|
+
extra["profile_setup_url"] = setup_url
|
|
53
|
+
if response.headers.get("x-imagcon-api-key"):
|
|
54
|
+
extra["api_key_issued"] = True
|
|
55
|
+
return extra
|
|
56
|
+
|
|
57
|
+
def _call(
|
|
58
|
+
self,
|
|
59
|
+
url: str,
|
|
60
|
+
*,
|
|
61
|
+
files: dict | None = None,
|
|
62
|
+
data: dict | None = None,
|
|
63
|
+
json: dict | None = None,
|
|
64
|
+
) -> tuple[bytes, dict]:
|
|
65
|
+
response = self._x402.post(
|
|
66
|
+
url, files=files, data=data, json=json, headers=self._profile_headers()
|
|
67
|
+
)
|
|
68
|
+
return response.content, self._extract_profile(response)
|
|
69
|
+
|
|
70
|
+
def generate_icons(self, image_bytes: bytes, filename: str) -> tuple[bytes, dict]:
|
|
71
|
+
return self._call(
|
|
72
|
+
self.ICONS_URL,
|
|
73
|
+
files={"image": (filename, image_bytes, "image/png")},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def generate_splash(
|
|
77
|
+
self,
|
|
78
|
+
image_bytes: bytes,
|
|
79
|
+
filename: str,
|
|
80
|
+
background_color: str = "#ffffff",
|
|
81
|
+
) -> tuple[bytes, dict]:
|
|
82
|
+
return self._call(
|
|
83
|
+
self.SPLASH_URL,
|
|
84
|
+
files={"image": (filename, image_bytes, "image/png")},
|
|
85
|
+
data={"background_color": background_color},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def generate_image_ai(self, description: str) -> tuple[dict, dict]:
|
|
89
|
+
content, extra = self._call(self.GENERATE_IMAGE_URL, json={"description": description})
|
|
90
|
+
return _json.loads(content), extra
|
|
91
|
+
|
|
92
|
+
def generate_pwa_icons_ai(self, description: str) -> tuple[bytes, dict]:
|
|
93
|
+
return self._call(self.GENERATE_ICONS_AI_URL, json={"description": description})
|
|
94
|
+
|
|
95
|
+
def generate_splash_screens_ai(
|
|
96
|
+
self, description: str, background_color: str = "#ffffff"
|
|
97
|
+
) -> tuple[bytes, dict]:
|
|
98
|
+
return self._call(
|
|
99
|
+
self.GENERATE_SPLASH_AI_URL,
|
|
100
|
+
json={"description": description, "background_color": background_color},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_wallet_profile(self) -> dict:
|
|
104
|
+
response = self._x402.get(f"{self.PROFILE_LOOKUP_URL}/{self.wallet_address}")
|
|
105
|
+
return response.json()
|
|
106
|
+
|
|
107
|
+
def setup_wallet_profile(
|
|
108
|
+
self,
|
|
109
|
+
profile_token: str,
|
|
110
|
+
name: str,
|
|
111
|
+
*,
|
|
112
|
+
terms_confirmed: bool,
|
|
113
|
+
company_name: str | None = None,
|
|
114
|
+
tax_id: str | None = None,
|
|
115
|
+
) -> dict:
|
|
116
|
+
# Imagcon requires proof of wallet control: EIP-191 signature over
|
|
117
|
+
# "imagcon.app/profile/activate:{token}:{unix_ts}", ±300s server window.
|
|
118
|
+
message = f"imagcon.app/profile/activate:{profile_token}:{int(time.time())}"
|
|
119
|
+
body: dict = {
|
|
120
|
+
"profile_token": profile_token,
|
|
121
|
+
"wallet_address": self.wallet_address,
|
|
122
|
+
"name": name,
|
|
123
|
+
"terms_confirmed": terms_confirmed,
|
|
124
|
+
"signature": self._x402.sign_message(message),
|
|
125
|
+
"message": message,
|
|
126
|
+
}
|
|
127
|
+
if company_name:
|
|
128
|
+
body["company_name"] = company_name
|
|
129
|
+
if tax_id:
|
|
130
|
+
body["tax_id"] = tax_id
|
|
131
|
+
response = self._x402.post(self.PROFILE_ACTIVATE_URL, json=body)
|
|
132
|
+
return response.json()
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
self._x402.close()
|
stackapps_mcp/main.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from stackapps_mcp.imagcon import ImagconX402Client
|
|
8
|
+
from stackapps_mcp.server import mcp, set_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _resolve_wallet_key(cli_key: str | None) -> str:
|
|
12
|
+
key = (cli_key or os.environ.get("WALLET_PRIVATE_KEY") or "").strip()
|
|
13
|
+
if not key:
|
|
14
|
+
print(
|
|
15
|
+
"Missing wallet private key. Set WALLET_PRIVATE_KEY or pass --wallet-key.\n"
|
|
16
|
+
"You need an EVM wallet with USDC on Base mainnet (eip155:8453).",
|
|
17
|
+
file=sys.stderr,
|
|
18
|
+
)
|
|
19
|
+
raise SystemExit(2)
|
|
20
|
+
if not key.startswith("0x"):
|
|
21
|
+
key = "0x" + key
|
|
22
|
+
return key
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
parser = argparse.ArgumentParser(prog="stackapps-suite-mcp")
|
|
27
|
+
parser.add_argument("--wallet-key", default=None)
|
|
28
|
+
parser.add_argument("--network", default="eip155:8453")
|
|
29
|
+
args, remaining = parser.parse_known_args()
|
|
30
|
+
sys.argv = [sys.argv[0], *remaining]
|
|
31
|
+
|
|
32
|
+
key = _resolve_wallet_key(args.wallet_key)
|
|
33
|
+
client = ImagconX402Client(key, args.network)
|
|
34
|
+
set_client(client)
|
|
35
|
+
try:
|
|
36
|
+
mcp.run()
|
|
37
|
+
finally:
|
|
38
|
+
client.close()
|
stackapps_mcp/server.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
from starlette.applications import Starlette
|
|
9
|
+
from starlette.middleware import Middleware
|
|
10
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import PlainTextResponse, Response
|
|
13
|
+
from starlette.routing import Mount, Route
|
|
14
|
+
|
|
15
|
+
from stackapps_mcp.imagcon import ImagconX402Client
|
|
16
|
+
|
|
17
|
+
mcp = FastMCP("stackapps-suite-mcp", stateless_http=True)
|
|
18
|
+
|
|
19
|
+
_client: ImagconX402Client | None = None
|
|
20
|
+
_blueprint_text = (Path(__file__).parent / "blueprint.txt").read_text(encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_client(client: ImagconX402Client) -> None:
|
|
24
|
+
global _client
|
|
25
|
+
_client = client
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _require_client() -> ImagconX402Client:
|
|
29
|
+
if _client is None:
|
|
30
|
+
raise RuntimeError("Imagcon x402 client is not configured")
|
|
31
|
+
return _client
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _profile_note(extra: dict) -> str:
|
|
35
|
+
if not extra.get("is_first_payment"):
|
|
36
|
+
return ""
|
|
37
|
+
parts = [
|
|
38
|
+
"A 60-day temp account was created for your wallet. To keep it permanently (free), "
|
|
39
|
+
"call setup_wallet_profile with the profile token from secure storage, or visit the activate URL."
|
|
40
|
+
]
|
|
41
|
+
if url := extra.get("profile_setup_url"):
|
|
42
|
+
parts.append(f"Activate URL: {url}")
|
|
43
|
+
if extra.get("api_key_issued"):
|
|
44
|
+
parts.append(
|
|
45
|
+
"An Imagcon API key was issued — use imagcon-mcp with that key (store securely, not in chat)."
|
|
46
|
+
)
|
|
47
|
+
return "\n\n" + " ".join(parts)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _safe_extractall(zf: zipfile.ZipFile, output_dir: Path) -> None:
|
|
51
|
+
dest = output_dir.resolve()
|
|
52
|
+
for info in zf.infolist():
|
|
53
|
+
if ".." in Path(info.filename).parts or info.filename.startswith("/"):
|
|
54
|
+
raise ValueError(f"Unsafe path in zip: {info.filename}")
|
|
55
|
+
target = (dest / info.filename).resolve()
|
|
56
|
+
try:
|
|
57
|
+
target.relative_to(dest)
|
|
58
|
+
except ValueError:
|
|
59
|
+
raise ValueError(f"Unsafe path in zip: {info.filename}") from None
|
|
60
|
+
zf.extractall(dest)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- tools ---
|
|
64
|
+
|
|
65
|
+
@mcp.tool()
|
|
66
|
+
def create_pwa_icons_from_image(
|
|
67
|
+
image_path: str,
|
|
68
|
+
output_dir: str = "./public/icons",
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Generate a full PWA icon set (all sizes, manifest.json) from a local PNG/JPEG/WebP image via the Imagcon x402 anonymous endpoint. Costs $0.10 USDC. output_dir should be an absolute path."""
|
|
71
|
+
path = Path(image_path)
|
|
72
|
+
image_bytes = path.read_bytes()
|
|
73
|
+
zip_bytes, extra = _require_client().generate_icons(image_bytes, path.name)
|
|
74
|
+
|
|
75
|
+
out = Path(output_dir)
|
|
76
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
78
|
+
count = len(zf.namelist())
|
|
79
|
+
_safe_extractall(zf, out)
|
|
80
|
+
|
|
81
|
+
return f"Generated {count} files in {out.resolve()}." + _profile_note(extra)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
def create_splash_screens_from_image(
|
|
86
|
+
image_path: str,
|
|
87
|
+
background_color: str = "#ffffff",
|
|
88
|
+
output_dir: str = "./public/splash",
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Generate 16 iOS/iPad splash screens from a local PNG/JPEG/WebP image via the Imagcon x402 anonymous endpoint. Costs $0.10 USDC. output_dir should be an absolute path."""
|
|
91
|
+
path = Path(image_path)
|
|
92
|
+
image_bytes = path.read_bytes()
|
|
93
|
+
zip_bytes, extra = _require_client().generate_splash(
|
|
94
|
+
image_bytes, path.name, background_color=background_color
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
out = Path(output_dir)
|
|
98
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
100
|
+
count = len(zf.namelist())
|
|
101
|
+
_safe_extractall(zf, out)
|
|
102
|
+
|
|
103
|
+
return f"Generated {count} files in {out.resolve()}." + _profile_note(extra)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@mcp.tool()
|
|
107
|
+
def generate_pwa_icons(
|
|
108
|
+
description: str,
|
|
109
|
+
output_dir: str = "./public/icons",
|
|
110
|
+
) -> str:
|
|
111
|
+
"""AI-generate a complete PWA icon set from a text description of your app. Costs $0.295 USDC. Imagcon generates the source image and all PWA sizes. output_dir should be an absolute path."""
|
|
112
|
+
zip_bytes, extra = _require_client().generate_pwa_icons_ai(description)
|
|
113
|
+
|
|
114
|
+
out = Path(output_dir)
|
|
115
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
117
|
+
count = len(zf.namelist())
|
|
118
|
+
_safe_extractall(zf, out)
|
|
119
|
+
|
|
120
|
+
return f"Generated {count} files in {out.resolve()}." + _profile_note(extra)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.tool()
|
|
124
|
+
def generate_image(description: str) -> str:
|
|
125
|
+
"""AI-generate a source image from a text description. Costs $0.195 USDC. Returns image_key and preview_url — useful for previewing before generating a full icon set."""
|
|
126
|
+
result, extra = _require_client().generate_image_ai(description)
|
|
127
|
+
msg = f"image_key: {result.get('image_key', '')}\npreview_url: {result.get('preview_url', '')}"
|
|
128
|
+
return msg + _profile_note(extra)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
def generate_splash_screens(
|
|
133
|
+
description: str,
|
|
134
|
+
background_color: str = "#ffffff",
|
|
135
|
+
output_dir: str = "./public/splash",
|
|
136
|
+
) -> str:
|
|
137
|
+
"""AI-generate 16 iOS/iPad splash screens from a text description of your app. Costs $0.295 USDC. output_dir should be an absolute path."""
|
|
138
|
+
zip_bytes, extra = _require_client().generate_splash_screens_ai(description, background_color)
|
|
139
|
+
|
|
140
|
+
out = Path(output_dir)
|
|
141
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
143
|
+
count = len(zf.namelist())
|
|
144
|
+
_safe_extractall(zf, out)
|
|
145
|
+
|
|
146
|
+
return f"Generated {count} files in {out.resolve()}." + _profile_note(extra)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
def get_wallet_profile() -> str:
|
|
151
|
+
"""Look up the Imagcon wallet profile for the configured wallet. Returns temp vs permanent status before prompting the user to activate."""
|
|
152
|
+
result = _require_client().get_wallet_profile()
|
|
153
|
+
profile = result.get("profile", result)
|
|
154
|
+
status = profile.get("status", "unknown")
|
|
155
|
+
name = profile.get("name", "")
|
|
156
|
+
parts = [f"Wallet {_require_client().wallet_address}: status={status}"]
|
|
157
|
+
if name:
|
|
158
|
+
parts.append(f"name={name}")
|
|
159
|
+
return ". ".join(parts) + "."
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@mcp.tool()
|
|
163
|
+
def setup_wallet_profile(
|
|
164
|
+
name: str,
|
|
165
|
+
terms_confirmed: bool,
|
|
166
|
+
profile_token: str | None = None,
|
|
167
|
+
company_name: str | None = None,
|
|
168
|
+
tax_id: str | None = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
"""Activate a permanent Imagcon account linked to your wallet. terms_confirmed must be true only after the human has read https://imagcon.app/terms-of-service and agreed. profile_token defaults to the session token from the first paid call if omitted."""
|
|
171
|
+
if not terms_confirmed:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"terms_confirmed must be true only after the human has read "
|
|
174
|
+
"https://imagcon.app/terms-of-service and agreed."
|
|
175
|
+
)
|
|
176
|
+
client = _require_client()
|
|
177
|
+
token = profile_token or client.profile_token
|
|
178
|
+
if not token:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"No profile token available. Complete a paid tool call first or pass profile_token "
|
|
181
|
+
"from secure storage."
|
|
182
|
+
)
|
|
183
|
+
result = client.setup_wallet_profile(
|
|
184
|
+
profile_token=token,
|
|
185
|
+
name=name,
|
|
186
|
+
terms_confirmed=terms_confirmed,
|
|
187
|
+
company_name=company_name,
|
|
188
|
+
tax_id=tax_id,
|
|
189
|
+
)
|
|
190
|
+
status = result.get("profile", {}).get("status", "permanent")
|
|
191
|
+
return f"Profile activated. Future payments from your wallet are linked to {name}. Status: {status}."
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- HTTP app: blueprint routes + SSE guard (for Cloud Run) ---
|
|
195
|
+
|
|
196
|
+
async def _serve_blueprint(request: Request) -> Response:
|
|
197
|
+
return PlainTextResponse(_blueprint_text, media_type="text/plain; charset=utf-8")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _SSEGuard(BaseHTTPMiddleware):
|
|
201
|
+
async def dispatch(self, request: Request, call_next):
|
|
202
|
+
if "text/event-stream" in request.headers.get("accept", ""):
|
|
203
|
+
return PlainTextResponse(
|
|
204
|
+
"SSE transport is not supported on this server.\n"
|
|
205
|
+
"Install the local stdio package instead:\n"
|
|
206
|
+
" uvx stackapps-suite-mcp --wallet-key 0x...",
|
|
207
|
+
status_code=405,
|
|
208
|
+
)
|
|
209
|
+
return await call_next(request)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def create_app(*, discovery_only: bool = False):
|
|
213
|
+
"""ASGI app for Cloud Run (discovery_only=True) or local HTTP MCP."""
|
|
214
|
+
routes: list = [
|
|
215
|
+
Route("/blueprint.txt", _serve_blueprint),
|
|
216
|
+
Route("/.well-known/blueprint.txt", _serve_blueprint),
|
|
217
|
+
]
|
|
218
|
+
if not discovery_only:
|
|
219
|
+
routes.append(Mount("/", app=mcp.streamable_http_app()))
|
|
220
|
+
return Starlette(
|
|
221
|
+
routes=routes,
|
|
222
|
+
middleware=[Middleware(_SSEGuard)],
|
|
223
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from eth_account import Account
|
|
7
|
+
from eth_account.messages import encode_defunct
|
|
8
|
+
from x402 import NoMatchingRequirementsError, max_amount, x402ClientSync
|
|
9
|
+
from x402.http.x402_http_client import x402HTTPClientSync
|
|
10
|
+
from x402.mechanisms.evm.exact import ExactEvmClientScheme
|
|
11
|
+
|
|
12
|
+
# Every member app's x402 endpoint pays to this wallet. The client refuses to
|
|
13
|
+
# sign a payment to any other address — a compromised or spoofed endpoint
|
|
14
|
+
# cannot redirect funds. Never change without verifying against a live 402
|
|
15
|
+
# challenge from a suite endpoint.
|
|
16
|
+
SUITE_PAY_TO = "0x1f2A484ef654d49c58c625b09e78B538501D652D"
|
|
17
|
+
|
|
18
|
+
_DEFAULT_MAX_USD_PER_CALL = 0.50
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _max_usdc_units() -> int:
|
|
22
|
+
raw = (os.environ.get("STACKAPPS_MAX_USD_PER_CALL") or "").strip()
|
|
23
|
+
try:
|
|
24
|
+
usd = float(raw) if raw else _DEFAULT_MAX_USD_PER_CALL
|
|
25
|
+
except ValueError:
|
|
26
|
+
usd = _DEFAULT_MAX_USD_PER_CALL
|
|
27
|
+
return int(usd * 1_000_000)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _suite_pay_to_only(version, reqs):
|
|
31
|
+
return [r for r in reqs if r.pay_to.lower() == SUITE_PAY_TO.lower()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SuiteX402Http:
|
|
35
|
+
"""Shared x402 HTTP engine for all StackApps member apps.
|
|
36
|
+
|
|
37
|
+
Handles the 402 challenge/sign/retry cycle with two guards applied before
|
|
38
|
+
any payment is signed: the challenge must pay SUITE_PAY_TO, and the amount
|
|
39
|
+
must not exceed the per-call cap (STACKAPPS_MAX_USD_PER_CALL, default $0.50).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, private_key: str, network: str = "eip155:8453") -> None:
|
|
43
|
+
account = Account.from_key(private_key)
|
|
44
|
+
self._account = account
|
|
45
|
+
self._wallet_address = account.address.lower()
|
|
46
|
+
x402_client = x402ClientSync()
|
|
47
|
+
x402_client.register(network, ExactEvmClientScheme(signer=account))
|
|
48
|
+
x402_client.register_policy(_suite_pay_to_only)
|
|
49
|
+
x402_client.register_policy(max_amount(_max_usdc_units()))
|
|
50
|
+
self._x402_http = x402HTTPClientSync(x402_client)
|
|
51
|
+
self._http = httpx.Client(timeout=300.0)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def wallet_address(self) -> str:
|
|
55
|
+
return self._wallet_address
|
|
56
|
+
|
|
57
|
+
def sign_message(self, text: str) -> str:
|
|
58
|
+
"""EIP-191 personal_sign over a UTF-8 string; returns 0x-prefixed hex."""
|
|
59
|
+
signed = self._account.sign_message(encode_defunct(text=text))
|
|
60
|
+
sig = signed.signature.hex()
|
|
61
|
+
return sig if sig.startswith("0x") else "0x" + sig
|
|
62
|
+
|
|
63
|
+
def post(
|
|
64
|
+
self,
|
|
65
|
+
url: str,
|
|
66
|
+
*,
|
|
67
|
+
files: dict | None = None,
|
|
68
|
+
data: dict | None = None,
|
|
69
|
+
json: dict | None = None,
|
|
70
|
+
headers: dict[str, str] | None = None,
|
|
71
|
+
) -> httpx.Response:
|
|
72
|
+
headers = dict(headers or {})
|
|
73
|
+
response = self._http.post(url, files=files, data=data, json=json, headers=headers)
|
|
74
|
+
if response.status_code == 402:
|
|
75
|
+
try:
|
|
76
|
+
payment_headers, _ = self._x402_http.handle_402_response(
|
|
77
|
+
dict(response.headers), response.content
|
|
78
|
+
)
|
|
79
|
+
except NoMatchingRequirementsError as e:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
f"Refused to pay {url}: the payment challenge does not pay the "
|
|
82
|
+
f"StackApps suite wallet ({SUITE_PAY_TO}) or exceeds the per-call cap "
|
|
83
|
+
f"(${_max_usdc_units() / 1_000_000:.2f} USDC). No payment was signed. "
|
|
84
|
+
f"Details: {e}"
|
|
85
|
+
) from e
|
|
86
|
+
response = self._http.post(
|
|
87
|
+
url, files=files, data=data, json=json, headers={**headers, **payment_headers}
|
|
88
|
+
)
|
|
89
|
+
if response.status_code >= 400:
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
f"x402 endpoint error {response.status_code} from {url}: {response.content[:200]}"
|
|
92
|
+
)
|
|
93
|
+
return response
|
|
94
|
+
|
|
95
|
+
def get(self, url: str) -> httpx.Response:
|
|
96
|
+
response = self._http.get(url)
|
|
97
|
+
if response.status_code >= 400:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
f"x402 endpoint error {response.status_code} from {url}: {response.content[:200]}"
|
|
100
|
+
)
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
self._http.close()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackapps-suite-mcp
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: MCP gateway for the StackApps tool suite — pay per call in USDC via x402, no account required
|
|
5
|
+
Project-URL: Homepage, https://stackapps.app
|
|
6
|
+
Project-URL: Source, https://github.com/Explorer-64/StackApps
|
|
7
|
+
Project-URL: Blueprint, https://mcp.stackapps.app/blueprint.txt
|
|
8
|
+
Author: Abe Reimer
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agent-payments,mcp,pwa-icons,stackapps,usdc,x402
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: eth-account>=0.13.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: mcp>=1.27.1
|
|
22
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
23
|
+
Requires-Dist: x402[clients,evm]>=2.0.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# stackapps-suite-mcp
|
|
27
|
+
|
|
28
|
+
mcp-name: io.github.explorer-64/stackapps-suite-mcp
|
|
29
|
+
|
|
30
|
+
One MCP server for the whole [StackApps](https://stackapps.app) tool suite.
|
|
31
|
+
Pay per call in USDC on Base mainnet via [x402](https://www.x402.org/) — no
|
|
32
|
+
account, no API key, no signup. Each member app (Imagcon today; more as they
|
|
33
|
+
ship) contributes tools; one install covers the suite.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add stackapps-suite -- uvx stackapps-suite-mcp --wallet-key 0x...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or in any MCP client config (Claude Desktop, Cursor, Windsurf, Cline):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"stackapps-suite": {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["stackapps-suite-mcp"],
|
|
49
|
+
"env": { "WALLET_PRIVATE_KEY": "0x..." }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requirements: [uv](https://docs.astral.sh/uv/), Python 3.11+, an EVM wallet
|
|
56
|
+
holding USDC on Base mainnet (eip155:8453). Prefer the `WALLET_PRIVATE_KEY`
|
|
57
|
+
env var over the `--wallet-key` argument.
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
| Tool | Price (USDC) | What it does |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `create_pwa_icons_from_image` | $0.10 | Full PWA icon set (27 files + manifest.json) from a local image |
|
|
64
|
+
| `create_splash_screens_from_image` | $0.10 | 16 iOS/iPad splash screens from a local image |
|
|
65
|
+
| `generate_pwa_icons` | $0.295 | AI-generated PWA icon set from a text description |
|
|
66
|
+
| `generate_image` | $0.195 | AI-generated source image (returns preview URL) |
|
|
67
|
+
| `generate_splash_screens` | $0.295 | AI-generated splash screens from a text description |
|
|
68
|
+
| `get_wallet_profile` | free | Check your wallet's Imagcon profile status |
|
|
69
|
+
| `setup_wallet_profile` | free | Activate a permanent account (wallet-signature verified) |
|
|
70
|
+
|
|
71
|
+
## Payment safety
|
|
72
|
+
|
|
73
|
+
The server refuses to sign any payment challenge that does not pay the
|
|
74
|
+
StackApps suite wallet, or that exceeds the per-call cap ($0.50 USDC by
|
|
75
|
+
default; override with `STACKAPPS_MAX_USD_PER_CALL`). A compromised or spoofed
|
|
76
|
+
endpoint cannot redirect or inflate payments. Payments settle on-chain via
|
|
77
|
+
EIP-3009 — the gas is sponsored; your wallet needs only USDC.
|
|
78
|
+
|
|
79
|
+
Never commit your wallet key, and never run this server on a public host with
|
|
80
|
+
`WALLET_PRIVATE_KEY` set. The hosted endpoint at
|
|
81
|
+
[mcp.stackapps.app](https://mcp.stackapps.app/blueprint.txt) serves discovery
|
|
82
|
+
metadata only.
|
|
83
|
+
|
|
84
|
+
## Discovery
|
|
85
|
+
|
|
86
|
+
Machine-readable capability contract (Blueprint Protocol):
|
|
87
|
+
[mcp.stackapps.app/blueprint.txt](https://mcp.stackapps.app/blueprint.txt)
|
|
88
|
+
|
|
89
|
+
## Links
|
|
90
|
+
|
|
91
|
+
- Suite: https://stackapps.app
|
|
92
|
+
- Source: https://github.com/Explorer-64/StackApps (`stackapps-mcp/`)
|
|
93
|
+
- Imagcon (current member app): https://imagcon.app
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
stackapps_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
stackapps_mcp/__main__.py,sha256=a5CB2dDZ8e5r66OVElwtjgg5LK7I_vjhKeV2tcGjM5Q,75
|
|
3
|
+
stackapps_mcp/blueprint.txt,sha256=44-Zi7Wge31isqro5y4dFJXHGYRwmpPU7_TwSeSiMJg,14858
|
|
4
|
+
stackapps_mcp/http_main.py,sha256=6y_BrtWwN3ZOBh2ekMuMM4I_M-YN6UPx3PTAYrutDWA,450
|
|
5
|
+
stackapps_mcp/imagcon.py,sha256=rbwUr5Bwm-yrk0aKIv7GMaWBh4UvjRa4fRvKV_wewxY,4909
|
|
6
|
+
stackapps_mcp/main.py,sha256=nebp3Ohm4SsLClDawHkxyxYzHEfd40iG4NiaBICxR9o,1117
|
|
7
|
+
stackapps_mcp/server.py,sha256=nRdN1zkahs83mooCpuCg0WDjtKkqXY3zZVcrg_cdbxk,8351
|
|
8
|
+
stackapps_mcp/x402_http.py,sha256=f75IzCXRwcEbKcory7JqwxsMZq25fKiXoq7P64dXFTM,4054
|
|
9
|
+
stackapps_suite_mcp-0.1.5.dist-info/METADATA,sha256=IkiK5Zxo0DLsROEYhDiUKCOXEmdgz4cTDmmjEGznHQk,3525
|
|
10
|
+
stackapps_suite_mcp-0.1.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
stackapps_suite_mcp-0.1.5.dist-info/entry_points.txt,sha256=FPq6hJduJbs8mQwX-n8c8hRqQrQBi77FZ6AnZy3CvT8,68
|
|
12
|
+
stackapps_suite_mcp-0.1.5.dist-info/RECORD,,
|