@kylewadegrove/cutline-mcp-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/dist/auth/callback.d.ts +5 -0
- package/dist/auth/callback.js +94 -0
- package/dist/auth/keychain.d.ts +3 -0
- package/dist/auth/keychain.js +20 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +75 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +31 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +90 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +17 -0
- package/package.json +42 -0
- package/src/auth/callback.ts +102 -0
- package/src/auth/keychain.ts +16 -0
- package/src/commands/login.ts +89 -0
- package/src/commands/logout.ts +30 -0
- package/src/commands/status.ts +106 -0
- package/src/index.ts +31 -0
- package/src/utils/config.ts +21 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Cutline MCP CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for authenticating with Cutline MCP servers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd functions/cutline/mcp-cli
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
npm link # Makes cutline-mcp available globally
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Login
|
|
17
|
+
|
|
18
|
+
Authenticate with Cutline and store credentials securely in your OS keychain:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cutline-mcp login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will:
|
|
25
|
+
1. Open your browser to Cutline's authentication page
|
|
26
|
+
2. After you log in, receive a refresh token
|
|
27
|
+
3. Store the token securely in your system keychain
|
|
28
|
+
4. Display confirmation with your email
|
|
29
|
+
|
|
30
|
+
### Check Status
|
|
31
|
+
|
|
32
|
+
View your current authentication status:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cutline-mcp status
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Shows:
|
|
39
|
+
- Whether you're authenticated
|
|
40
|
+
- Your email and user ID
|
|
41
|
+
- Token expiration time
|
|
42
|
+
- Subscription status (coming soon)
|
|
43
|
+
|
|
44
|
+
### Logout
|
|
45
|
+
|
|
46
|
+
Remove stored credentials:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cutline-mcp logout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
### Security
|
|
55
|
+
|
|
56
|
+
- **Keychain Storage**: Tokens are stored in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
|
57
|
+
- **Refresh Tokens**: Long-lived refresh tokens are stored, not short-lived ID tokens
|
|
58
|
+
- **Automatic Refresh**: MCP servers automatically exchange refresh tokens for fresh ID tokens
|
|
59
|
+
- **Revocable**: Tokens can be revoked from your Cutline account settings
|
|
60
|
+
|
|
61
|
+
### Authentication Flow
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
1. User runs: cutline-mcp login
|
|
65
|
+
2. CLI starts local callback server on localhost:8765
|
|
66
|
+
3. CLI opens browser to: https://cutline.app/mcp-auth?callback=http://localhost:8765
|
|
67
|
+
4. User logs in to Cutline (or is already logged in)
|
|
68
|
+
5. Cutline redirects to: http://localhost:8765?token=REFRESH_TOKEN&email=user@example.com
|
|
69
|
+
6. CLI receives token and stores in keychain
|
|
70
|
+
7. CLI displays success message
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### MCP Server Integration
|
|
74
|
+
|
|
75
|
+
MCP servers automatically read tokens from the keychain:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// In utils.ts
|
|
79
|
+
async function requirePremium(authToken?: string) {
|
|
80
|
+
// Priority: explicit token > keychain > error
|
|
81
|
+
let token = authToken;
|
|
82
|
+
|
|
83
|
+
if (!token) {
|
|
84
|
+
const refreshToken = await getStoredRefreshToken();
|
|
85
|
+
if (refreshToken) {
|
|
86
|
+
token = await exchangeRefreshToken(refreshToken);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!token) {
|
|
91
|
+
throw new Error("Run 'cutline-mcp login' to authenticate");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ... validate token and subscription
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
### Environment Variables
|
|
101
|
+
|
|
102
|
+
- `CUTLINE_AUTH_URL`: Override auth endpoint (default: `https://cutline.app/mcp-auth`)
|
|
103
|
+
- `FIREBASE_API_KEY`: Firebase API key for token exchange
|
|
104
|
+
|
|
105
|
+
### Callback Port
|
|
106
|
+
|
|
107
|
+
The CLI uses port `8765` for the OAuth callback. If this port is in use, you'll see an error. Close other applications and try again.
|
|
108
|
+
|
|
109
|
+
## Troubleshooting
|
|
110
|
+
|
|
111
|
+
### "Port 8765 is already in use"
|
|
112
|
+
|
|
113
|
+
Another application is using the callback port. Find and close it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
lsof -i :8765
|
|
117
|
+
kill -9 <PID>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### "Authentication timeout"
|
|
121
|
+
|
|
122
|
+
The browser didn't complete the OAuth flow within 5 minutes. Try again:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cutline-mcp login
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### "Failed to refresh token"
|
|
129
|
+
|
|
130
|
+
Your stored token may be invalid or revoked. Log out and log in again:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cutline-mcp logout
|
|
134
|
+
cutline-mcp login
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Keychain Access Denied
|
|
138
|
+
|
|
139
|
+
On macOS, you may need to grant Terminal/IDE access to Keychain:
|
|
140
|
+
|
|
141
|
+
1. Open Keychain Access app
|
|
142
|
+
2. Find "cutline-mcp" entry
|
|
143
|
+
3. Right-click ā Get Info ā Access Control
|
|
144
|
+
4. Add your Terminal/IDE to allowed applications
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
### Build
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm run build
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Watch Mode
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm run dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Test Locally
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm link
|
|
164
|
+
cutline-mcp --help
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Next Steps
|
|
168
|
+
|
|
169
|
+
- [ ] Add web app `/mcp-auth` endpoint
|
|
170
|
+
- [ ] Implement device registration in Firestore
|
|
171
|
+
- [ ] Add subscription status to `status` command
|
|
172
|
+
- [ ] Publish to npm as `@cutline/mcp-cli`
|
|
173
|
+
- [ ] Create standalone binaries for macOS/Windows/Linux
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startCallbackServer = startCallbackServer;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const CALLBACK_PORT = 8765;
|
|
9
|
+
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
async function startCallbackServer() {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const app = (0, express_1.default)();
|
|
13
|
+
let server;
|
|
14
|
+
// Timeout handler
|
|
15
|
+
const timeout = setTimeout(() => {
|
|
16
|
+
server?.close();
|
|
17
|
+
reject(new Error('Authentication timeout - no callback received'));
|
|
18
|
+
}, TIMEOUT_MS);
|
|
19
|
+
// Callback endpoint
|
|
20
|
+
app.get('/', (req, res) => {
|
|
21
|
+
const token = req.query.token;
|
|
22
|
+
const email = req.query.email;
|
|
23
|
+
if (!token) {
|
|
24
|
+
res.status(400).send('Missing token parameter');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Send success page
|
|
28
|
+
res.send(`
|
|
29
|
+
<!DOCTYPE html>
|
|
30
|
+
<html>
|
|
31
|
+
<head>
|
|
32
|
+
<title>Cutline MCP - Authentication Successful</title>
|
|
33
|
+
<style>
|
|
34
|
+
body {
|
|
35
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
align-items: center;
|
|
39
|
+
height: 100vh;
|
|
40
|
+
margin: 0;
|
|
41
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
42
|
+
}
|
|
43
|
+
.container {
|
|
44
|
+
background: white;
|
|
45
|
+
padding: 3rem;
|
|
46
|
+
border-radius: 1rem;
|
|
47
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
48
|
+
text-align: center;
|
|
49
|
+
max-width: 400px;
|
|
50
|
+
}
|
|
51
|
+
h1 {
|
|
52
|
+
color: #667eea;
|
|
53
|
+
margin-bottom: 1rem;
|
|
54
|
+
}
|
|
55
|
+
p {
|
|
56
|
+
color: #666;
|
|
57
|
+
line-height: 1.6;
|
|
58
|
+
}
|
|
59
|
+
.checkmark {
|
|
60
|
+
font-size: 4rem;
|
|
61
|
+
color: #4CAF50;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div class="container">
|
|
67
|
+
<div class="checkmark">ā</div>
|
|
68
|
+
<h1>Authentication Successful!</h1>
|
|
69
|
+
<p>You can now close this window and return to your terminal.</p>
|
|
70
|
+
${email ? `<p>Logged in as: <strong>${email}</strong></p>` : ''}
|
|
71
|
+
</div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
`);
|
|
75
|
+
// Clean up and resolve
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
server.close();
|
|
78
|
+
resolve({ token, email });
|
|
79
|
+
});
|
|
80
|
+
// Start server
|
|
81
|
+
server = app.listen(CALLBACK_PORT, () => {
|
|
82
|
+
console.log(`Callback server listening on http://localhost:${CALLBACK_PORT}`);
|
|
83
|
+
});
|
|
84
|
+
server.on('error', (err) => {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
if (err.code === 'EADDRINUSE') {
|
|
87
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close other applications and try again.`));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
reject(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.storeRefreshToken = storeRefreshToken;
|
|
7
|
+
exports.getRefreshToken = getRefreshToken;
|
|
8
|
+
exports.deleteRefreshToken = deleteRefreshToken;
|
|
9
|
+
const keytar_1 = __importDefault(require("keytar"));
|
|
10
|
+
const SERVICE_NAME = 'cutline-mcp';
|
|
11
|
+
const ACCOUNT_NAME = 'refresh-token';
|
|
12
|
+
async function storeRefreshToken(token) {
|
|
13
|
+
await keytar_1.default.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
14
|
+
}
|
|
15
|
+
async function getRefreshToken() {
|
|
16
|
+
return await keytar_1.default.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
17
|
+
}
|
|
18
|
+
async function deleteRefreshToken() {
|
|
19
|
+
return await keytar_1.default.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
20
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loginCommand = loginCommand;
|
|
7
|
+
const open_1 = __importDefault(require("open"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const callback_js_1 = require("../auth/callback.js");
|
|
11
|
+
const keychain_js_1 = require("../auth/keychain.js");
|
|
12
|
+
const config_js_1 = require("../utils/config.js");
|
|
13
|
+
async function exchangeCustomToken(customToken, apiKey) {
|
|
14
|
+
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
token: customToken,
|
|
19
|
+
returnSecureToken: true,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const error = await response.text();
|
|
24
|
+
throw new Error(`Failed to exchange custom token: ${error}`);
|
|
25
|
+
}
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return {
|
|
28
|
+
refreshToken: data.refreshToken,
|
|
29
|
+
email: data.email,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function loginCommand(options) {
|
|
33
|
+
const config = (0, config_js_1.getConfig)(options);
|
|
34
|
+
console.log(chalk_1.default.bold('\nš Cutline MCP Authentication\n'));
|
|
35
|
+
if (options.staging) {
|
|
36
|
+
console.log(chalk_1.default.yellow(' ā ļø Using STAGING environment\n'));
|
|
37
|
+
}
|
|
38
|
+
if (!config.FIREBASE_API_KEY) {
|
|
39
|
+
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
40
|
+
console.error(chalk_1.default.red(`Error: ${varName} environment variable is required.`));
|
|
41
|
+
console.error(chalk_1.default.gray('Please set it before running this command:'));
|
|
42
|
+
console.error(chalk_1.default.cyan(` export ${varName}=AIzaSy...`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const spinner = (0, ora_1.default)('Starting authentication flow...').start();
|
|
46
|
+
try {
|
|
47
|
+
// Start callback server
|
|
48
|
+
spinner.text = 'Waiting for authentication...';
|
|
49
|
+
const serverPromise = (0, callback_js_1.startCallbackServer)();
|
|
50
|
+
// Open browser
|
|
51
|
+
const authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
52
|
+
await (0, open_1.default)(authUrl);
|
|
53
|
+
spinner.text = 'Browser opened - please complete authentication';
|
|
54
|
+
// Wait for callback with custom token
|
|
55
|
+
const result = await serverPromise;
|
|
56
|
+
// Exchange custom token for refresh token
|
|
57
|
+
spinner.text = 'Exchanging token...';
|
|
58
|
+
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
59
|
+
// Store refresh token
|
|
60
|
+
await (0, keychain_js_1.storeRefreshToken)(refreshToken);
|
|
61
|
+
spinner.succeed(chalk_1.default.green('Successfully authenticated!'));
|
|
62
|
+
if (email || result.email) {
|
|
63
|
+
console.log(chalk_1.default.gray(` Logged in as: ${email || result.email}`));
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk_1.default.gray(' MCP servers can now access your account\n'));
|
|
66
|
+
console.log(chalk_1.default.dim(' Run'), chalk_1.default.cyan('cutline-mcp status'), chalk_1.default.dim('to verify\n'));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
spinner.fail(chalk_1.default.red('Authentication failed'));
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
console.error(chalk_1.default.red(` ${error.message}\n`));
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function logoutCommand(): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logoutCommand = logoutCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const keychain_js_1 = require("../auth/keychain.js");
|
|
10
|
+
async function logoutCommand() {
|
|
11
|
+
console.log(chalk_1.default.bold('\nš Logging out of Cutline MCP\n'));
|
|
12
|
+
const spinner = (0, ora_1.default)('Removing stored credentials...').start();
|
|
13
|
+
try {
|
|
14
|
+
const deleted = await (0, keychain_js_1.deleteRefreshToken)();
|
|
15
|
+
if (deleted) {
|
|
16
|
+
spinner.succeed(chalk_1.default.green('Successfully logged out'));
|
|
17
|
+
console.log(chalk_1.default.gray(' Credentials removed from keychain\n'));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
spinner.info(chalk_1.default.yellow('No credentials found'));
|
|
21
|
+
console.log(chalk_1.default.gray(' You were not logged in\n'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
spinner.fail(chalk_1.default.red('Logout failed'));
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
console.error(chalk_1.default.red(` ${error.message}\n`));
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.statusCommand = statusCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const firebase_admin_1 = __importDefault(require("firebase-admin"));
|
|
10
|
+
const keychain_js_1 = require("../auth/keychain.js");
|
|
11
|
+
const config_js_1 = require("../utils/config.js");
|
|
12
|
+
async function exchangeRefreshToken(refreshToken, apiKey) {
|
|
13
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
grant_type: 'refresh_token',
|
|
18
|
+
refresh_token: refreshToken,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const errorText = await response.text();
|
|
23
|
+
let errorMessage = 'Failed to refresh token';
|
|
24
|
+
try {
|
|
25
|
+
const errorData = JSON.parse(errorText);
|
|
26
|
+
errorMessage = errorData.error?.message || errorText;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
errorMessage = errorText;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(errorMessage);
|
|
32
|
+
}
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
return data.id_token;
|
|
35
|
+
}
|
|
36
|
+
async function statusCommand(options) {
|
|
37
|
+
console.log(chalk_1.default.bold('\nš Cutline MCP Status\n'));
|
|
38
|
+
const spinner = (0, ora_1.default)('Checking authentication...').start();
|
|
39
|
+
try {
|
|
40
|
+
// Check for stored refresh token
|
|
41
|
+
const refreshToken = await (0, keychain_js_1.getRefreshToken)();
|
|
42
|
+
if (!refreshToken) {
|
|
43
|
+
spinner.info(chalk_1.default.yellow('Not authenticated'));
|
|
44
|
+
console.log(chalk_1.default.gray(' Run'), chalk_1.default.cyan('cutline-mcp login'), chalk_1.default.gray('to authenticate\n'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Get config for API key
|
|
48
|
+
const config = (0, config_js_1.getConfig)(options);
|
|
49
|
+
if (!config.FIREBASE_API_KEY) {
|
|
50
|
+
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
51
|
+
spinner.fail(chalk_1.default.red('Configuration error'));
|
|
52
|
+
console.error(chalk_1.default.red(` ${varName} environment variable is required.`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Exchange refresh token for ID token
|
|
56
|
+
spinner.text = 'Verifying credentials...';
|
|
57
|
+
const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
|
|
58
|
+
// Initialize Firebase Admin with correct project ID
|
|
59
|
+
if (firebase_admin_1.default.apps.length === 0) {
|
|
60
|
+
const projectId = options.staging ? 'demo-makerkit' : 'makerkit-todo-app';
|
|
61
|
+
firebase_admin_1.default.initializeApp({ projectId });
|
|
62
|
+
}
|
|
63
|
+
// Verify the ID token
|
|
64
|
+
const decoded = await firebase_admin_1.default.auth().verifyIdToken(idToken);
|
|
65
|
+
spinner.succeed(chalk_1.default.green('Authenticated'));
|
|
66
|
+
console.log(chalk_1.default.gray(' User:'), chalk_1.default.white(decoded.email || decoded.uid));
|
|
67
|
+
console.log(chalk_1.default.gray(' UID:'), chalk_1.default.dim(decoded.uid));
|
|
68
|
+
// Calculate token expiry
|
|
69
|
+
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
|
|
70
|
+
console.log(chalk_1.default.gray(' Token expires in:'), chalk_1.default.white(`${expiresIn} minutes`));
|
|
71
|
+
// Show custom claims if present
|
|
72
|
+
if (decoded.mcp) {
|
|
73
|
+
console.log(chalk_1.default.gray(' MCP enabled:'), chalk_1.default.green('ā'));
|
|
74
|
+
}
|
|
75
|
+
if (decoded.deviceId) {
|
|
76
|
+
console.log(chalk_1.default.gray(' Device ID:'), chalk_1.default.dim(decoded.deviceId));
|
|
77
|
+
}
|
|
78
|
+
// TODO: Check subscription status from Firestore
|
|
79
|
+
console.log(chalk_1.default.gray(' Subscription:'), chalk_1.default.dim('(checking...)'));
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
spinner.fail(chalk_1.default.red('Status check failed'));
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
console.error(chalk_1.default.red(` ${error.message}`));
|
|
86
|
+
console.log(chalk_1.default.gray(' Try running'), chalk_1.default.cyan('cutline-mcp login'), chalk_1.default.gray('again\n'));
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const login_js_1 = require("./commands/login.js");
|
|
6
|
+
const logout_js_1 = require("./commands/logout.js");
|
|
7
|
+
const status_js_1 = require("./commands/status.js");
|
|
8
|
+
const program = new commander_1.Command();
|
|
9
|
+
program
|
|
10
|
+
.name('cutline-mcp')
|
|
11
|
+
.description('CLI tool for authenticating with Cutline MCP servers')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
program
|
|
14
|
+
.command('login')
|
|
15
|
+
.description('Authenticate with Cutline and store credentials')
|
|
16
|
+
.option('--staging', 'Use staging environment')
|
|
17
|
+
.action(login_js_1.loginCommand);
|
|
18
|
+
program
|
|
19
|
+
.command('logout')
|
|
20
|
+
.description('Remove stored credentials')
|
|
21
|
+
.action(logout_js_1.logoutCommand);
|
|
22
|
+
program
|
|
23
|
+
.command('status')
|
|
24
|
+
.description('Show current authentication status')
|
|
25
|
+
.option('--staging', 'Use staging environment')
|
|
26
|
+
.action(status_js_1.statusCommand);
|
|
27
|
+
program.parse();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConfig = getConfig;
|
|
4
|
+
function getConfig(options = {}) {
|
|
5
|
+
if (options.staging) {
|
|
6
|
+
return {
|
|
7
|
+
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
|
|
8
|
+
CALLBACK_URL: 'http://localhost:8765',
|
|
9
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY_STAGING || process.env.FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
|
|
14
|
+
CALLBACK_URL: 'http://localhost:8765',
|
|
15
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || 'AIzaSyDW7846oQfvFU3Vnc1DELj4XdlvvYFjaIU',
|
|
16
|
+
};
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kylewadegrove/cutline-mcp-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for authenticating with Cutline MCP servers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cutline-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cutline",
|
|
16
|
+
"mcp",
|
|
17
|
+
"cli",
|
|
18
|
+
"authentication"
|
|
19
|
+
],
|
|
20
|
+
"author": "Cutline",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^11.1.0",
|
|
24
|
+
"keytar": "^7.9.0",
|
|
25
|
+
"open": "^9.1.0",
|
|
26
|
+
"express": "^4.18.2",
|
|
27
|
+
"firebase-admin": "^12.0.0",
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"ora": "^5.4.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/express": "^4.17.21",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import type { Server } from 'http';
|
|
3
|
+
|
|
4
|
+
const CALLBACK_PORT = 8765;
|
|
5
|
+
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
|
|
7
|
+
export interface CallbackResult {
|
|
8
|
+
token: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function startCallbackServer(): Promise<CallbackResult> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const app = express();
|
|
15
|
+
let server: Server;
|
|
16
|
+
|
|
17
|
+
// Timeout handler
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
server?.close();
|
|
20
|
+
reject(new Error('Authentication timeout - no callback received'));
|
|
21
|
+
}, TIMEOUT_MS);
|
|
22
|
+
|
|
23
|
+
// Callback endpoint
|
|
24
|
+
app.get('/', (req, res) => {
|
|
25
|
+
const token = req.query.token as string;
|
|
26
|
+
const email = req.query.email as string;
|
|
27
|
+
|
|
28
|
+
if (!token) {
|
|
29
|
+
res.status(400).send('Missing token parameter');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Send success page
|
|
34
|
+
res.send(`
|
|
35
|
+
<!DOCTYPE html>
|
|
36
|
+
<html>
|
|
37
|
+
<head>
|
|
38
|
+
<title>Cutline MCP - Authentication Successful</title>
|
|
39
|
+
<style>
|
|
40
|
+
body {
|
|
41
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
align-items: center;
|
|
45
|
+
height: 100vh;
|
|
46
|
+
margin: 0;
|
|
47
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
48
|
+
}
|
|
49
|
+
.container {
|
|
50
|
+
background: white;
|
|
51
|
+
padding: 3rem;
|
|
52
|
+
border-radius: 1rem;
|
|
53
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
54
|
+
text-align: center;
|
|
55
|
+
max-width: 400px;
|
|
56
|
+
}
|
|
57
|
+
h1 {
|
|
58
|
+
color: #667eea;
|
|
59
|
+
margin-bottom: 1rem;
|
|
60
|
+
}
|
|
61
|
+
p {
|
|
62
|
+
color: #666;
|
|
63
|
+
line-height: 1.6;
|
|
64
|
+
}
|
|
65
|
+
.checkmark {
|
|
66
|
+
font-size: 4rem;
|
|
67
|
+
color: #4CAF50;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div class="container">
|
|
73
|
+
<div class="checkmark">ā</div>
|
|
74
|
+
<h1>Authentication Successful!</h1>
|
|
75
|
+
<p>You can now close this window and return to your terminal.</p>
|
|
76
|
+
${email ? `<p>Logged in as: <strong>${email}</strong></p>` : ''}
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
// Clean up and resolve
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
server.close();
|
|
85
|
+
resolve({ token, email });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Start server
|
|
89
|
+
server = app.listen(CALLBACK_PORT, () => {
|
|
90
|
+
console.log(`Callback server listening on http://localhost:${CALLBACK_PORT}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
server.on('error', (err: any) => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
if (err.code === 'EADDRINUSE') {
|
|
96
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close other applications and try again.`));
|
|
97
|
+
} else {
|
|
98
|
+
reject(err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import keytar from 'keytar';
|
|
2
|
+
|
|
3
|
+
const SERVICE_NAME = 'cutline-mcp';
|
|
4
|
+
const ACCOUNT_NAME = 'refresh-token';
|
|
5
|
+
|
|
6
|
+
export async function storeRefreshToken(token: string): Promise<void> {
|
|
7
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
11
|
+
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function deleteRefreshToken(): Promise<boolean> {
|
|
15
|
+
return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
16
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { startCallbackServer } from '../auth/callback.js';
|
|
5
|
+
import { storeRefreshToken } from '../auth/keychain.js';
|
|
6
|
+
import { getConfig } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
|
|
9
|
+
const response = await fetch(
|
|
10
|
+
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`,
|
|
11
|
+
{
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
token: customToken,
|
|
16
|
+
returnSecureToken: true,
|
|
17
|
+
}),
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const error = await response.text();
|
|
23
|
+
throw new Error(`Failed to exchange custom token: ${error}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return {
|
|
28
|
+
refreshToken: data.refreshToken,
|
|
29
|
+
email: data.email,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function loginCommand(options: { staging?: boolean }) {
|
|
34
|
+
const config = getConfig(options);
|
|
35
|
+
console.log(chalk.bold('\nš Cutline MCP Authentication\n'));
|
|
36
|
+
|
|
37
|
+
if (options.staging) {
|
|
38
|
+
console.log(chalk.yellow(' ā ļø Using STAGING environment\n'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!config.FIREBASE_API_KEY) {
|
|
42
|
+
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
43
|
+
console.error(chalk.red(`Error: ${varName} environment variable is required.`));
|
|
44
|
+
console.error(chalk.gray('Please set it before running this command:'));
|
|
45
|
+
console.error(chalk.cyan(` export ${varName}=AIzaSy...`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const spinner = ora('Starting authentication flow...').start();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Start callback server
|
|
53
|
+
spinner.text = 'Waiting for authentication...';
|
|
54
|
+
const serverPromise = startCallbackServer();
|
|
55
|
+
|
|
56
|
+
// Open browser
|
|
57
|
+
const authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
58
|
+
await open(authUrl);
|
|
59
|
+
spinner.text = 'Browser opened - please complete authentication';
|
|
60
|
+
|
|
61
|
+
// Wait for callback with custom token
|
|
62
|
+
const result = await serverPromise;
|
|
63
|
+
|
|
64
|
+
// Exchange custom token for refresh token
|
|
65
|
+
spinner.text = 'Exchanging token...';
|
|
66
|
+
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
67
|
+
|
|
68
|
+
// Store refresh token
|
|
69
|
+
await storeRefreshToken(refreshToken);
|
|
70
|
+
|
|
71
|
+
spinner.succeed(chalk.green('Successfully authenticated!'));
|
|
72
|
+
|
|
73
|
+
if (email || result.email) {
|
|
74
|
+
console.log(chalk.gray(` Logged in as: ${email || result.email}`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.gray(' MCP servers can now access your account\n'));
|
|
78
|
+
console.log(chalk.dim(' Run'), chalk.cyan('cutline-mcp status'), chalk.dim('to verify\n'));
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
spinner.fail(chalk.red('Authentication failed'));
|
|
82
|
+
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
console.error(chalk.red(` ${error.message}\n`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { deleteRefreshToken } from '../auth/keychain.js';
|
|
4
|
+
|
|
5
|
+
export async function logoutCommand() {
|
|
6
|
+
console.log(chalk.bold('\nš Logging out of Cutline MCP\n'));
|
|
7
|
+
|
|
8
|
+
const spinner = ora('Removing stored credentials...').start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const deleted = await deleteRefreshToken();
|
|
12
|
+
|
|
13
|
+
if (deleted) {
|
|
14
|
+
spinner.succeed(chalk.green('Successfully logged out'));
|
|
15
|
+
console.log(chalk.gray(' Credentials removed from keychain\n'));
|
|
16
|
+
} else {
|
|
17
|
+
spinner.info(chalk.yellow('No credentials found'));
|
|
18
|
+
console.log(chalk.gray(' You were not logged in\n'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
} catch (error) {
|
|
22
|
+
spinner.fail(chalk.red('Logout failed'));
|
|
23
|
+
|
|
24
|
+
if (error instanceof Error) {
|
|
25
|
+
console.error(chalk.red(` ${error.message}\n`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import admin from 'firebase-admin';
|
|
4
|
+
import { getRefreshToken } from '../auth/keychain.js';
|
|
5
|
+
import { getConfig } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
async function exchangeRefreshToken(refreshToken: string, apiKey: string): Promise<string> {
|
|
8
|
+
const response = await fetch(
|
|
9
|
+
`https://securetoken.googleapis.com/v1/token?key=${apiKey}`,
|
|
10
|
+
{
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
grant_type: 'refresh_token',
|
|
15
|
+
refresh_token: refreshToken,
|
|
16
|
+
}),
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const errorText = await response.text();
|
|
22
|
+
let errorMessage = 'Failed to refresh token';
|
|
23
|
+
try {
|
|
24
|
+
const errorData = JSON.parse(errorText);
|
|
25
|
+
errorMessage = errorData.error?.message || errorText;
|
|
26
|
+
} catch {
|
|
27
|
+
errorMessage = errorText;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(errorMessage);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
return data.id_token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function statusCommand(options: { staging?: boolean }) {
|
|
37
|
+
console.log(chalk.bold('\nš Cutline MCP Status\n'));
|
|
38
|
+
|
|
39
|
+
const spinner = ora('Checking authentication...').start();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Check for stored refresh token
|
|
43
|
+
const refreshToken = await getRefreshToken();
|
|
44
|
+
|
|
45
|
+
if (!refreshToken) {
|
|
46
|
+
spinner.info(chalk.yellow('Not authenticated'));
|
|
47
|
+
console.log(chalk.gray(' Run'), chalk.cyan('cutline-mcp login'), chalk.gray('to authenticate\n'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get config for API key
|
|
52
|
+
const config = getConfig(options);
|
|
53
|
+
|
|
54
|
+
if (!config.FIREBASE_API_KEY) {
|
|
55
|
+
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
56
|
+
spinner.fail(chalk.red('Configuration error'));
|
|
57
|
+
console.error(chalk.red(` ${varName} environment variable is required.`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exchange refresh token for ID token
|
|
62
|
+
spinner.text = 'Verifying credentials...';
|
|
63
|
+
const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
|
|
64
|
+
|
|
65
|
+
// Initialize Firebase Admin with correct project ID
|
|
66
|
+
if (admin.apps.length === 0) {
|
|
67
|
+
const projectId = options.staging ? 'demo-makerkit' : 'makerkit-todo-app';
|
|
68
|
+
admin.initializeApp({ projectId });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify the ID token
|
|
72
|
+
const decoded = await admin.auth().verifyIdToken(idToken);
|
|
73
|
+
|
|
74
|
+
spinner.succeed(chalk.green('Authenticated'));
|
|
75
|
+
|
|
76
|
+
console.log(chalk.gray(' User:'), chalk.white(decoded.email || decoded.uid));
|
|
77
|
+
console.log(chalk.gray(' UID:'), chalk.dim(decoded.uid));
|
|
78
|
+
|
|
79
|
+
// Calculate token expiry
|
|
80
|
+
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
|
|
81
|
+
console.log(chalk.gray(' Token expires in:'), chalk.white(`${expiresIn} minutes`));
|
|
82
|
+
|
|
83
|
+
// Show custom claims if present
|
|
84
|
+
if (decoded.mcp) {
|
|
85
|
+
console.log(chalk.gray(' MCP enabled:'), chalk.green('ā'));
|
|
86
|
+
}
|
|
87
|
+
if (decoded.deviceId) {
|
|
88
|
+
console.log(chalk.gray(' Device ID:'), chalk.dim(decoded.deviceId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// TODO: Check subscription status from Firestore
|
|
92
|
+
console.log(chalk.gray(' Subscription:'), chalk.dim('(checking...)'));
|
|
93
|
+
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
spinner.fail(chalk.red('Status check failed'));
|
|
98
|
+
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
console.error(chalk.red(` ${error.message}`));
|
|
101
|
+
console.log(chalk.gray(' Try running'), chalk.cyan('cutline-mcp login'), chalk.gray('again\n'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { loginCommand } from './commands/login.js';
|
|
4
|
+
import { logoutCommand } from './commands/logout.js';
|
|
5
|
+
import { statusCommand } from './commands/status.js';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('cutline-mcp')
|
|
11
|
+
.description('CLI tool for authenticating with Cutline MCP servers')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('login')
|
|
16
|
+
.description('Authenticate with Cutline and store credentials')
|
|
17
|
+
.option('--staging', 'Use staging environment')
|
|
18
|
+
.action(loginCommand);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('logout')
|
|
22
|
+
.description('Remove stored credentials')
|
|
23
|
+
.action(logoutCommand);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('status')
|
|
27
|
+
.description('Show current authentication status')
|
|
28
|
+
.option('--staging', 'Use staging environment')
|
|
29
|
+
.action(statusCommand);
|
|
30
|
+
|
|
31
|
+
program.parse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
AUTH_URL: string;
|
|
3
|
+
CALLBACK_URL: string;
|
|
4
|
+
FIREBASE_API_KEY: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getConfig(options: { staging?: boolean } = {}): Config {
|
|
8
|
+
if (options.staging) {
|
|
9
|
+
return {
|
|
10
|
+
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
|
|
11
|
+
CALLBACK_URL: 'http://localhost:8765',
|
|
12
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY_STAGING || process.env.FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
|
|
18
|
+
CALLBACK_URL: 'http://localhost:8765',
|
|
19
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || 'AIzaSyDW7846oQfvFU3Vnc1DELj4XdlvvYFjaIU',
|
|
20
|
+
};
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist"
|
|
21
|
+
]
|
|
22
|
+
}
|