n2-stitch-mcp 1.0.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/LICENSE +21 -0
- package/README.md +224 -0
- package/index.js +228 -0
- package/package.json +54 -0
- package/src/auth.js +113 -0
- package/src/config.js +60 -0
- package/src/generation-tracker.js +277 -0
- package/src/proxy-client.js +226 -0
- package/src/server.js +255 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nton2 (https://nton2.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# 🛡️ N2 Stitch MCP — Resilient Proxy for Google Stitch
|
|
2
|
+
|
|
3
|
+
> **Never lose a screen generation again.** Built by [Nton2](https://nton2.com)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/n2-stitch-mcp)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
A lightweight, resilient MCP proxy server for [Google Stitch](https://stitch.withgoogle.com/). While other Stitch MCP servers silently fail when connections drop during long-running screen generations, **N2 Stitch MCP keeps going**.
|
|
9
|
+
|
|
10
|
+
## 🔥 The Problem
|
|
11
|
+
|
|
12
|
+
Google Stitch's `generate_screen_from_text` takes **2–10 minutes** to create a screen. But the API **drops the TCP connection after ~60 seconds**.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Other MCP servers:
|
|
16
|
+
Request → 60s → TCP dropped → ❌ "Error: connection reset" → Your work is LOST
|
|
17
|
+
|
|
18
|
+
N2 Stitch MCP:
|
|
19
|
+
Request → 60s → TCP dropped → 🛡️ Auto-recovery → Polling... → ✅ Screen delivered!
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 🆚 Why N2 Stitch MCP?
|
|
23
|
+
|
|
24
|
+
| Feature | Official CLI | Other MCPs | **N2 Stitch MCP** |
|
|
25
|
+
|---------|:---:|:---:|:---:|
|
|
26
|
+
| TCP Drop Recovery | ❌ | ❌ | ✅ **Auto-polling** |
|
|
27
|
+
| Generation Tracking | ❌ | ❌ | ✅ `generation_status` |
|
|
28
|
+
| Generation List | ❌ | ❌ | ✅ `list_generations` |
|
|
29
|
+
| Exponential Backoff | ❌ | ❌ | ✅ **3x retry + jitter** |
|
|
30
|
+
| Auto Token Refresh | ✅ | ⚠️ | ✅ **Background refresh** |
|
|
31
|
+
| Test Suite | ? | ❌ | ✅ **33 tests** |
|
|
32
|
+
| Lightweight | ❌ (heavy CLI) | ✅ | ✅ **Pure proxy** |
|
|
33
|
+
| npx support | ✅ | ✅ | ✅ |
|
|
34
|
+
|
|
35
|
+
## 🚀 Quick Start
|
|
36
|
+
|
|
37
|
+
### 1. Prerequisites
|
|
38
|
+
|
|
39
|
+
You need **one** of these for authentication:
|
|
40
|
+
|
|
41
|
+
- **Option A**: Google Cloud SDK (recommended)
|
|
42
|
+
```bash
|
|
43
|
+
# Install gcloud
|
|
44
|
+
# Windows: winget install Google.CloudSDK
|
|
45
|
+
# macOS: brew install --cask google-cloud-sdk
|
|
46
|
+
# Linux: curl https://sdk.cloud.google.com | bash
|
|
47
|
+
|
|
48
|
+
# Login
|
|
49
|
+
gcloud auth application-default login
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **Option B**: API Key
|
|
53
|
+
```bash
|
|
54
|
+
export STITCH_API_KEY="your-api-key"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Add to MCP Client
|
|
58
|
+
|
|
59
|
+
Add this to your MCP configuration (Cursor, Claude Desktop, Gemini CLI, Antigravity, etc.):
|
|
60
|
+
|
|
61
|
+
**With gcloud (recommended):**
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"n2-stitch": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "n2-stitch-mcp"]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**With API Key:**
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"n2-stitch": {
|
|
78
|
+
"command": "npx",
|
|
79
|
+
"args": ["-y", "n2-stitch-mcp"],
|
|
80
|
+
"env": {
|
|
81
|
+
"STITCH_API_KEY": "your-api-key-here"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's it! Your AI agent can now use Google Stitch. 🎉
|
|
89
|
+
|
|
90
|
+
### 3. Setup Wizard (Optional)
|
|
91
|
+
|
|
92
|
+
Run the interactive setup to verify everything:
|
|
93
|
+
```bash
|
|
94
|
+
npx -y n2-stitch-mcp init
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 🛡️ 3-Layer Safety Architecture
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
┌─────────────────────────────────────────────────────────┐
|
|
101
|
+
│ Your AI Agent │
|
|
102
|
+
│ "Create a mobile login screen" │
|
|
103
|
+
└────────────────────┬────────────────────────────────────┘
|
|
104
|
+
│ MCP (STDIO)
|
|
105
|
+
┌────────────────────▼────────────────────────────────────┐
|
|
106
|
+
│ N2 Stitch MCP Proxy │
|
|
107
|
+
│ │
|
|
108
|
+
│ ┌──────────────────────────────────────────────────┐ │
|
|
109
|
+
│ │ L1: Exponential Backoff Retry │ │
|
|
110
|
+
│ │ Network errors → retry 3x (1s→2s→4s ±jitter)│ │
|
|
111
|
+
│ ├──────────────────────────────────────────────────┤ │
|
|
112
|
+
│ │ L2: Auto Token Refresh │ │
|
|
113
|
+
│ │ 401 response → refresh token → retry │ │
|
|
114
|
+
│ ├──────────────────────────────────────────────────┤ │
|
|
115
|
+
│ │ L3: TCP Drop Recovery │ │
|
|
116
|
+
│ │ Connection lost → poll list_screens every │ │
|
|
117
|
+
│ │ 10s → detect new screen → return result │ │
|
|
118
|
+
│ └──────────────────────────────────────────────────┘ │
|
|
119
|
+
└────────────────────┬────────────────────────────────────┘
|
|
120
|
+
│ HTTPS
|
|
121
|
+
┌────────────────────▼────────────────────────────────────┐
|
|
122
|
+
│ Google Stitch API │
|
|
123
|
+
│ stitch.googleapis.com/mcp │
|
|
124
|
+
└─────────────────────────────────────────────────────────┘
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Layer 1: Exponential Backoff Retry
|
|
128
|
+
- Transient errors (ECONNRESET, timeout, 429, 503) → automatic retry
|
|
129
|
+
- 3 attempts with exponential backoff: 1s → 2s → 4s (±30% jitter)
|
|
130
|
+
- Non-transient errors (400, 404) fail immediately
|
|
131
|
+
|
|
132
|
+
### Layer 2: Auto Token Refresh
|
|
133
|
+
- Background token refresh every 50 minutes (before 60-min expiry)
|
|
134
|
+
- On 401 response → force refresh → retry the request
|
|
135
|
+
|
|
136
|
+
### Layer 3: TCP Drop Recovery (⭐ Unique!)
|
|
137
|
+
- When `generate_screen_from_text` connection drops:
|
|
138
|
+
1. Wait 5 seconds (let Stitch finish processing)
|
|
139
|
+
2. Poll `list_screens` every 10 seconds
|
|
140
|
+
3. Detect new screen by comparing before/after screen lists
|
|
141
|
+
4. Return the generated screen — **as if nothing happened!**
|
|
142
|
+
- Timeout: 12 minutes max polling
|
|
143
|
+
|
|
144
|
+
## 🔧 Available Tools
|
|
145
|
+
|
|
146
|
+
### Stitch API Tools (Auto-discovered)
|
|
147
|
+
| Tool | Description |
|
|
148
|
+
|------|-------------|
|
|
149
|
+
| `create_project` | Create a new Stitch project |
|
|
150
|
+
| `list_projects` | List all projects |
|
|
151
|
+
| `get_project` | Get project details |
|
|
152
|
+
| `list_screens` | List screens in a project |
|
|
153
|
+
| `get_screen` | Get screen details (HTML/CSS) |
|
|
154
|
+
| `generate_screen_from_text` | **✨ Generate UI from text (Resilient!)** |
|
|
155
|
+
| `edit_screens` | Edit existing screens |
|
|
156
|
+
| `generate_variants` | Generate design variants |
|
|
157
|
+
|
|
158
|
+
### Virtual Tools (N2 Exclusive)
|
|
159
|
+
| Tool | Description |
|
|
160
|
+
|------|-------------|
|
|
161
|
+
| `generation_status` | Check real-time status of a screen generation |
|
|
162
|
+
| `list_generations` | List all in-flight and recent generations |
|
|
163
|
+
|
|
164
|
+
## ⚙️ Environment Variables
|
|
165
|
+
|
|
166
|
+
| Variable | Description | Default |
|
|
167
|
+
|----------|-------------|---------|
|
|
168
|
+
| `STITCH_HOST` | Stitch API URL | `https://stitch.googleapis.com/mcp` |
|
|
169
|
+
| `STITCH_API_KEY` | API Key (alternative to gcloud) | — |
|
|
170
|
+
| `STITCH_PROJECT_ID` | GCP Project ID | — |
|
|
171
|
+
| `STITCH_DEBUG` | Enable debug logging (`1`) | `0` |
|
|
172
|
+
|
|
173
|
+
## 🧪 Tests
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npm test
|
|
177
|
+
# 33 passed, 0 failed ✅
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Tests cover:
|
|
181
|
+
- ✅ Configuration loading & defaults
|
|
182
|
+
- ✅ API key & ADC authentication
|
|
183
|
+
- ✅ Exponential backoff calculation
|
|
184
|
+
- ✅ Transient error detection
|
|
185
|
+
- ✅ Generation tracking state management
|
|
186
|
+
- ✅ Server module integration
|
|
187
|
+
|
|
188
|
+
## 📁 Project Structure
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
n2-stitch-mcp/
|
|
192
|
+
├── index.js # Entry point + setup wizard
|
|
193
|
+
├── src/
|
|
194
|
+
│ ├── config.js # Configuration from env vars
|
|
195
|
+
│ ├── auth.js # Google auth (ADC / API key)
|
|
196
|
+
│ ├── proxy-client.js # HTTP client (L1 + L2)
|
|
197
|
+
│ ├── generation-tracker.js # TCP drop recovery (L3)
|
|
198
|
+
│ └── server.js # MCP server + tool routing
|
|
199
|
+
├── test.js # Test suite (33 tests)
|
|
200
|
+
├── package.json
|
|
201
|
+
├── LICENSE
|
|
202
|
+
└── README.md
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 📊 Performance
|
|
206
|
+
|
|
207
|
+
| Metric | Value |
|
|
208
|
+
|--------|-------|
|
|
209
|
+
| Cold start | ~2 seconds |
|
|
210
|
+
| Memory usage | ~30 MB |
|
|
211
|
+
| Request overhead | < 10ms per proxy call |
|
|
212
|
+
| Dependencies | Only 2 (`@modelcontextprotocol/sdk`, `google-auth-library`) |
|
|
213
|
+
|
|
214
|
+
## 🤝 Contributing
|
|
215
|
+
|
|
216
|
+
Issues and PRs welcome at [GitHub](https://github.com/choihyunsus/n2-stitch-mcp).
|
|
217
|
+
|
|
218
|
+
## 📄 License
|
|
219
|
+
|
|
220
|
+
MIT — Use it freely, modify it, sell it, whatever you want.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
Made with ❤️ by [Nton2](https://nton2.com) — *Building the Body for AI*
|
package/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* N2 Stitch MCP — Entry Point
|
|
4
|
+
*
|
|
5
|
+
* ╔═══════════════════════════════════════════════════════════╗
|
|
6
|
+
* ║ N2 Stitch MCP Proxy Server ║
|
|
7
|
+
* ║ ║
|
|
8
|
+
* ║ A resilient STDIO MCP proxy for Google Stitch. ║
|
|
9
|
+
* ║ Built by the N2 AI Family (Rose 🌹 & Jennie 💎) ║
|
|
10
|
+
* ║ ║
|
|
11
|
+
* ║ 3-Layer Safety Architecture: ║
|
|
12
|
+
* ║ L1 — Exponential-backoff retry (network errors) ║
|
|
13
|
+
* ║ L2 — Auto token refresh on 401 ║
|
|
14
|
+
* ║ L3 — TCP drop recovery via polling for generation ║
|
|
15
|
+
* ╚═══════════════════════════════════════════════════════════╝
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node index.js # Run with gcloud ADC
|
|
19
|
+
* STITCH_API_KEY=xxx node index.js # Run with API key
|
|
20
|
+
* STITCH_DEBUG=1 node index.js # Enable debug logging
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { loadConfig, useApiKey } from './src/config.js';
|
|
25
|
+
import { AuthManager } from './src/auth.js';
|
|
26
|
+
import { ProxyClient } from './src/proxy-client.js';
|
|
27
|
+
import { GenerationTracker } from './src/generation-tracker.js';
|
|
28
|
+
import { StitchMCPServer } from './src/server.js';
|
|
29
|
+
|
|
30
|
+
// ── Logger (all output to stderr; stdout reserved for STDIO MCP) ──
|
|
31
|
+
|
|
32
|
+
const logger = {
|
|
33
|
+
info: (msg) => process.stderr.write(`[n2-stitch] INFO ${msg}\n`),
|
|
34
|
+
warn: (msg) => process.stderr.write(`[n2-stitch] WARN ${msg}\n`),
|
|
35
|
+
error: (msg) => process.stderr.write(`[n2-stitch] ERROR ${msg}\n`),
|
|
36
|
+
debug: (msg) => process.stderr.write(`[n2-stitch] DEBUG ${msg}\n`),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Main ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
// Handle init subcommand
|
|
43
|
+
if (process.argv[2] === 'init') {
|
|
44
|
+
await runInit();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
|
|
50
|
+
if (!config.debug) {
|
|
51
|
+
// In non-debug mode, suppress info/debug logs
|
|
52
|
+
logger.info = () => { };
|
|
53
|
+
logger.debug = () => { };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.info('Starting N2 Stitch MCP proxy server');
|
|
57
|
+
logger.info(`Stitch API: ${config.stitchHost}`);
|
|
58
|
+
|
|
59
|
+
// ── Step 1: Authentication ──
|
|
60
|
+
const auth = new AuthManager(config, logger);
|
|
61
|
+
try {
|
|
62
|
+
await auth.initialize();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
process.stderr.write(`\n[n2-stitch] FATAL: Authentication failed!\n`);
|
|
65
|
+
process.stderr.write(`[n2-stitch] ${err.message}\n\n`);
|
|
66
|
+
process.stderr.write(`[n2-stitch] To fix, run one of:\n`);
|
|
67
|
+
process.stderr.write(`[n2-stitch] 1. Set API key: $env:STITCH_API_KEY="your-key"\n`);
|
|
68
|
+
process.stderr.write(`[n2-stitch] 2. Install gcloud + run: gcloud auth application-default login\n`);
|
|
69
|
+
process.stderr.write(`[n2-stitch] 3. Run: node index.js init\n\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Step 2: Proxy client ──
|
|
74
|
+
const proxyClient = new ProxyClient(config, auth, logger);
|
|
75
|
+
|
|
76
|
+
// ── Step 3: Generation tracker ──
|
|
77
|
+
const genTracker = new GenerationTracker(proxyClient, config, logger);
|
|
78
|
+
|
|
79
|
+
// ── Step 4: MCP Server ──
|
|
80
|
+
const server = new StitchMCPServer(config, proxyClient, genTracker, logger);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await server.discoverAndRegisterTools();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
process.stderr.write(`\n[n2-stitch] FATAL: Failed to discover tools from Stitch API\n`);
|
|
86
|
+
process.stderr.write(`[n2-stitch] ${err.message}\n`);
|
|
87
|
+
process.stderr.write(`[n2-stitch] Check your network and authentication.\n\n`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Step 5: STDIO transport ──
|
|
92
|
+
const transport = new StdioServerTransport();
|
|
93
|
+
await server.getServer().connect(transport);
|
|
94
|
+
|
|
95
|
+
logger.info('STDIO MCP server ready — listening for JSON-RPC on stdin');
|
|
96
|
+
|
|
97
|
+
// Graceful shutdown
|
|
98
|
+
const shutdown = () => {
|
|
99
|
+
logger.info('Shutting down...');
|
|
100
|
+
auth.stop();
|
|
101
|
+
process.exit(0);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
process.on('SIGINT', shutdown);
|
|
105
|
+
process.on('SIGTERM', shutdown);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Init subcommand ─────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async function runInit() {
|
|
111
|
+
console.log(`
|
|
112
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
113
|
+
║ N2 Stitch MCP — Setup Wizard ║
|
|
114
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
115
|
+
`);
|
|
116
|
+
|
|
117
|
+
// Check gcloud
|
|
118
|
+
const { execSync } = await import('child_process');
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
execSync('gcloud --version', { stdio: 'pipe' });
|
|
122
|
+
console.log('✅ gcloud CLI found');
|
|
123
|
+
} catch {
|
|
124
|
+
console.log('❌ gcloud CLI not found');
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log('Install Google Cloud SDK:');
|
|
127
|
+
console.log(' Windows: winget install Google.CloudSDK');
|
|
128
|
+
console.log(' macOS: brew install --cask google-cloud-sdk');
|
|
129
|
+
console.log(' Linux: curl https://sdk.cloud.google.com | bash');
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log('After installing, run this command again.');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check ADC
|
|
136
|
+
try {
|
|
137
|
+
execSync('gcloud auth application-default print-access-token', { stdio: 'pipe' });
|
|
138
|
+
console.log('✅ Application Default Credentials found');
|
|
139
|
+
} catch {
|
|
140
|
+
console.log('⚠️ No Application Default Credentials found');
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log('Running: gcloud auth application-default login');
|
|
143
|
+
console.log('A browser window will open for Google Login...');
|
|
144
|
+
console.log('');
|
|
145
|
+
try {
|
|
146
|
+
execSync('gcloud auth application-default login', { stdio: 'inherit' });
|
|
147
|
+
console.log('✅ Authentication successful!');
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.log(`❌ Authentication failed: ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Test Stitch API
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log('Testing Stitch API connection...');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const token = execSync('gcloud auth application-default print-access-token', { encoding: 'utf-8' }).trim();
|
|
160
|
+
|
|
161
|
+
const resp = await fetch('https://stitch.googleapis.com/mcp', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
Authorization: `Bearer ${token}`,
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
jsonrpc: '2.0',
|
|
169
|
+
method: 'tools/list',
|
|
170
|
+
params: {},
|
|
171
|
+
id: 1,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (resp.ok) {
|
|
176
|
+
const json = await resp.json();
|
|
177
|
+
const toolCount = json.result?.tools?.length || 0;
|
|
178
|
+
console.log(`✅ Stitch API connected! Found ${toolCount} tools.`);
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`⚠️ Stitch API returned HTTP ${resp.status}`);
|
|
181
|
+
console.log(' You may need to enable the Stitch API in your GCP project.');
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(`⚠️ Could not reach Stitch API: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Print MCP configuration
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log('Add this to your MCP client configuration:');
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
mcpServers: {
|
|
195
|
+
'n2-stitch': {
|
|
196
|
+
command: 'npx',
|
|
197
|
+
args: ['-y', 'n2-stitch-mcp'],
|
|
198
|
+
env: {
|
|
199
|
+
STITCH_DEBUG: '1',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}, null, 2));
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('Or with API key (no gcloud needed):');
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(JSON.stringify({
|
|
208
|
+
mcpServers: {
|
|
209
|
+
'n2-stitch': {
|
|
210
|
+
command: 'npx',
|
|
211
|
+
args: ['-y', 'n2-stitch-mcp'],
|
|
212
|
+
env: {
|
|
213
|
+
STITCH_API_KEY: 'your-api-key-here',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
}, null, 2));
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log('Setup complete! 🎉');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Entry ───────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
main().catch(err => {
|
|
225
|
+
process.stderr.write(`[n2-stitch] FATAL: ${err.message}\n`);
|
|
226
|
+
process.stderr.write(`[n2-stitch] ${err.stack}\n`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n2-stitch-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Resilient MCP proxy for Google Stitch — 3-layer safety (auto-retry, token refresh, TCP drop recovery). Never lose a generation again!",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"n2-stitch-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/choihyunsus/n2-stitch-mcp"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://nton2.com",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/choihyunsus/n2-stitch-mcp/issues"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node index.js",
|
|
26
|
+
"test": "node test.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"google-stitch",
|
|
35
|
+
"stitch",
|
|
36
|
+
"ui-generation",
|
|
37
|
+
"ai-design",
|
|
38
|
+
"resilient-proxy",
|
|
39
|
+
"tcp-recovery",
|
|
40
|
+
"auto-retry",
|
|
41
|
+
"ai-tools",
|
|
42
|
+
"ai-agent",
|
|
43
|
+
"claude",
|
|
44
|
+
"cursor",
|
|
45
|
+
"antigravity",
|
|
46
|
+
"gemini"
|
|
47
|
+
],
|
|
48
|
+
"author": "Nton2 <nton2@nton2.com> (https://nton2.com)",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
52
|
+
"google-auth-library": "^9.15.1"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N2 Stitch MCP — Auth Module
|
|
3
|
+
*
|
|
4
|
+
* Manages Google Cloud authentication via:
|
|
5
|
+
* 1. API Key (simplest — set STITCH_API_KEY)
|
|
6
|
+
* 2. ADC (gcloud auth application-default login)
|
|
7
|
+
* 3. Service Account JSON (GOOGLE_APPLICATION_CREDENTIALS)
|
|
8
|
+
*
|
|
9
|
+
* Handles automatic token refresh before expiry.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
13
|
+
|
|
14
|
+
export class AuthManager {
|
|
15
|
+
/** @param {import('./config.js').loadConfig} config */
|
|
16
|
+
constructor(config, logger) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
|
|
20
|
+
this._accessToken = '';
|
|
21
|
+
this._expiresAt = 0;
|
|
22
|
+
this._refreshTimer = null;
|
|
23
|
+
this._googleAuth = null;
|
|
24
|
+
this._client = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Public ──────────────────────────────────────────────
|
|
28
|
+
async initialize() {
|
|
29
|
+
if (this.config.apiKey) {
|
|
30
|
+
this.logger.info('Using API key authentication');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.logger.info('Using Google ADC authentication');
|
|
35
|
+
|
|
36
|
+
this._googleAuth = new GoogleAuth({
|
|
37
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this._client = await this._googleAuth.getClient();
|
|
41
|
+
|
|
42
|
+
// Fetch initial token
|
|
43
|
+
await this._refreshToken();
|
|
44
|
+
|
|
45
|
+
// Start background refresh
|
|
46
|
+
this._startRefreshLoop();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the current access token (or empty string if API key mode).
|
|
51
|
+
*/
|
|
52
|
+
getAccessToken() {
|
|
53
|
+
return this._accessToken;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns headers suitable for Stitch API requests.
|
|
58
|
+
*/
|
|
59
|
+
getAuthHeaders() {
|
|
60
|
+
if (this.config.apiKey) {
|
|
61
|
+
return { 'x-goog-api-key': this.config.apiKey };
|
|
62
|
+
}
|
|
63
|
+
return { Authorization: `Bearer ${this._accessToken}` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Force-refresh the token (called on 401 responses).
|
|
68
|
+
*/
|
|
69
|
+
async forceRefresh() {
|
|
70
|
+
if (this.config.apiKey) return; // API keys don't refresh
|
|
71
|
+
this.logger.info('Force-refreshing access token...');
|
|
72
|
+
await this._refreshToken();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop the background refresh loop.
|
|
77
|
+
*/
|
|
78
|
+
stop() {
|
|
79
|
+
if (this._refreshTimer) {
|
|
80
|
+
clearInterval(this._refreshTimer);
|
|
81
|
+
this._refreshTimer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Private ─────────────────────────────────────────────
|
|
86
|
+
async _refreshToken() {
|
|
87
|
+
try {
|
|
88
|
+
const tokenResponse = await this._client.getAccessToken();
|
|
89
|
+
this._accessToken = tokenResponse.token || tokenResponse;
|
|
90
|
+
this._expiresAt = Date.now() + this.config.tokenRefreshIntervalMs;
|
|
91
|
+
this.logger.info('Access token refreshed successfully');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
this.logger.error(`Failed to refresh token: ${err.message}`);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_startRefreshLoop() {
|
|
99
|
+
// Refresh every tokenRefreshIntervalMs (50 min by default)
|
|
100
|
+
this._refreshTimer = setInterval(async () => {
|
|
101
|
+
try {
|
|
102
|
+
await this._refreshToken();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this.logger.error(`Background token refresh failed: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}, this.config.tokenRefreshIntervalMs);
|
|
107
|
+
|
|
108
|
+
// Don't let the timer block process exit
|
|
109
|
+
if (this._refreshTimer.unref) {
|
|
110
|
+
this._refreshTimer.unref();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|