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.
File without changes
@@ -0,0 +1,4 @@
1
+ from stackapps_mcp.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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()
@@ -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()
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ stackapps-suite-mcp = stackapps_mcp.__main__:main