@legionai/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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Legion MCP Server
2
+
3
+ Access deployment logs directly from AI agents like Claude Code, Cursor, or Windsurf.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @legion/mcp
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Add to your MCP config (e.g., `~/.config/claude/mcp.json`):
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "legion": {
19
+ "command": "legion-mcp",
20
+ "env": {
21
+ "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
22
+ "LEGION_ACCESS_TOKEN": "<your_token>"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Available Tools
30
+
31
+ | Tool | Description |
32
+ |------|-------------|
33
+ | `legion_list_projects` | List your connected projects |
34
+ | `legion_connect_project` | Connect a new project |
35
+ | `legion_get_logs` | Fetch logs with filters (level, limit) |
36
+ | `legion_get_errors` | Fetch error-level logs |
37
+ | `legion_disconnect_project` | Remove a project connection |
38
+
39
+ ## Usage Example
40
+
41
+ Once configured, ask your AI agent:
42
+
43
+ - "Check my logs for errors"
44
+ - "What's the latest error in my-api?"
45
+ - "Connect my project for debugging"
46
+
47
+ ## Security
48
+
49
+ - All log access is authenticated via your Legion account
50
+ - You can only access logs for projects you've connected
51
+ - Tokens are encrypted at rest
52
+ - Access is rate-limited (30 requests/minute)
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@legionai/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Legion MCP Server - Railway logs access for AI agents",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "legion-mcp": "./src/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "legion",
15
+ "railway",
16
+ "ai",
17
+ "agents"
18
+ ],
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.0.0"
22
+ }
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Legion CLI Login
5
+ *
6
+ * Opens browser for OAuth login, receives token via local callback.
7
+ *
8
+ * Usage: legion-mcp login
9
+ */
10
+
11
+ const http = require('http');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+ const { execSync } = require('child_process');
16
+
17
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.legion');
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
19
+
20
+ const LEGION_URL = process.env.LEGION_URL || 'https://legion-ai.org';
21
+
22
+ /**
23
+ * Get a random available port
24
+ */
25
+ function getRandomPort() {
26
+ return Math.floor(Math.random() * (65535 - 49152) + 49152);
27
+ }
28
+
29
+ /**
30
+ * Open URL in default browser
31
+ */
32
+ function openBrowser(url) {
33
+ const platform = process.platform;
34
+ let command;
35
+
36
+ if (platform === 'darwin') {
37
+ command = `open "${url}"`;
38
+ } else if (platform === 'win32') {
39
+ command = `start "${url}"`;
40
+ } else {
41
+ command = `xdg-open "${url}"`;
42
+ }
43
+
44
+ try {
45
+ execSync(command);
46
+ } catch (err) {
47
+ console.error('Failed to open browser. Please open this URL manually:');
48
+ console.error(url);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Save config to ~/.legion/config.json
54
+ */
55
+ function saveConfig(config) {
56
+ if (!fs.existsSync(CONFIG_DIR)) {
57
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
58
+ }
59
+
60
+ const existing = fs.existsSync(CONFIG_FILE)
61
+ ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
62
+ : {};
63
+
64
+ const merged = { ...existing, ...config };
65
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
66
+ }
67
+
68
+ /**
69
+ * Load config from ~/.legion/config.json
70
+ */
71
+ function loadConfig() {
72
+ if (!fs.existsSync(CONFIG_FILE)) {
73
+ return {};
74
+ }
75
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
76
+ }
77
+
78
+ /**
79
+ * Start local callback server and wait for token
80
+ */
81
+ function startCallbackServer(port, state) {
82
+ return new Promise((resolve, reject) => {
83
+ const timeout = setTimeout(() => {
84
+ server.close();
85
+ reject(new Error('Login timed out after 5 minutes'));
86
+ }, 5 * 60 * 1000);
87
+
88
+ const server = http.createServer((req, res) => {
89
+ const url = new URL(req.url, `http://localhost:${port}`);
90
+
91
+ if (url.pathname === '/callback') {
92
+ const token = url.searchParams.get('token');
93
+ const returnedState = url.searchParams.get('state');
94
+ const error = url.searchParams.get('error');
95
+
96
+ if (error) {
97
+ res.writeHead(200, { 'Content-Type': 'text/html' });
98
+ res.end(`
99
+ <html>
100
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
101
+ <h1 style="color: #ef4444;">Login Failed</h1>
102
+ <p>${error}</p>
103
+ <p>You can close this window.</p>
104
+ </body>
105
+ </html>
106
+ `);
107
+ clearTimeout(timeout);
108
+ server.close();
109
+ reject(new Error(error));
110
+ return;
111
+ }
112
+
113
+ if (returnedState !== state) {
114
+ res.writeHead(400, { 'Content-Type': 'text/html' });
115
+ res.end('<html><body>Invalid state token</body></html>');
116
+ return;
117
+ }
118
+
119
+ if (token) {
120
+ res.writeHead(200, { 'Content-Type': 'text/html' });
121
+ res.end(`
122
+ <html>
123
+ <body style="font-family: system-ui; padding: 40px; text-align: center; background: #0a0a0a; color: white;">
124
+ <h1 style="color: #22c55e;">✓ Logged in to Legion</h1>
125
+ <p style="color: #a1a1aa;">You can close this window and return to your terminal.</p>
126
+ </body>
127
+ </html>
128
+ `);
129
+ clearTimeout(timeout);
130
+ server.close();
131
+ resolve(token);
132
+ } else {
133
+ res.writeHead(400, { 'Content-Type': 'text/html' });
134
+ res.end('<html><body>No token received</body></html>');
135
+ }
136
+ } else {
137
+ res.writeHead(404);
138
+ res.end('Not found');
139
+ }
140
+ });
141
+
142
+ server.listen(port, () => {
143
+ console.log(`Waiting for login callback on port ${port}...`);
144
+ });
145
+
146
+ server.on('error', (err) => {
147
+ clearTimeout(timeout);
148
+ reject(err);
149
+ });
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Main login flow
155
+ */
156
+ async function login() {
157
+ console.log('🔐 Logging in to Legion...\n');
158
+
159
+ const port = getRandomPort();
160
+ const state = crypto.randomBytes(16).toString('hex');
161
+ const callback = `http://localhost:${port}/callback`;
162
+
163
+ const loginUrl = `${LEGION_URL}/cli-login?callback=${encodeURIComponent(callback)}&state=${state}`;
164
+
165
+ console.log('Opening browser for login...');
166
+ openBrowser(loginUrl);
167
+
168
+ try {
169
+ const token = await startCallbackServer(port, state);
170
+
171
+ // Save token to config
172
+ saveConfig({
173
+ access_token: token,
174
+ auth_server_url: LEGION_URL.replace('legion-ai.org', 'auth.legion-ai.org'),
175
+ });
176
+
177
+ console.log('\n✓ Logged in successfully!');
178
+ console.log(` Token saved to ${CONFIG_FILE}`);
179
+ console.log('\n You can now use Legion MCP tools.');
180
+
181
+ } catch (err) {
182
+ console.error('\n✗ Login failed:', err.message);
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Logout: remove saved token
189
+ */
190
+ function logout() {
191
+ if (fs.existsSync(CONFIG_FILE)) {
192
+ const config = loadConfig();
193
+ delete config.access_token;
194
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
195
+ console.log('✓ Logged out of Legion');
196
+ } else {
197
+ console.log('Not logged in');
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Show current status
203
+ */
204
+ function status() {
205
+ const config = loadConfig();
206
+ if (config.access_token) {
207
+ console.log('✓ Logged in to Legion');
208
+ console.log(` Token: ${config.access_token.substring(0, 10)}...`);
209
+ console.log(` Config: ${CONFIG_FILE}`);
210
+ } else {
211
+ console.log('✗ Not logged in');
212
+ console.log(' Run: legion-mcp login');
213
+ }
214
+ }
215
+
216
+ // CLI handler
217
+ const command = process.argv[2];
218
+
219
+ switch (command) {
220
+ case 'login':
221
+ login();
222
+ break;
223
+ case 'logout':
224
+ logout();
225
+ break;
226
+ case 'status':
227
+ status();
228
+ break;
229
+ default:
230
+ // If no command, run the MCP server (imported from index.js)
231
+ require('./index.js');
232
+ }
package/src/index.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Legion MCP Server
5
+ *
6
+ * Provides AI agents with access to deployment logs
7
+ * for iterative debugging and self-healing applications.
8
+ *
9
+ * Usage:
10
+ * legion-mcp --auth-server-url https://auth.yourlegion.com --token <legion_token>
11
+ *
12
+ * Or set environment variables:
13
+ * LEGION_AUTH_SERVER_URL
14
+ * LEGION_ACCESS_TOKEN
15
+ */
16
+
17
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
18
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
19
+ const {
20
+ CallToolRequestSchema,
21
+ ListToolsRequestSchema,
22
+ } = require('@modelcontextprotocol/sdk/types.js');
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ // Load config from ~/.legion/config.json
27
+ function loadConfig() {
28
+ const configPath = path.join(process.env.HOME || process.env.USERPROFILE, '.legion', 'config.json');
29
+ try {
30
+ if (fs.existsSync(configPath)) {
31
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
32
+ }
33
+ } catch (err) {
34
+ console.error('[LEGION MCP] Failed to load config:', err.message);
35
+ }
36
+ return {};
37
+ }
38
+
39
+ const config = loadConfig();
40
+
41
+ // Configuration (env vars take precedence over config file)
42
+ const AUTH_SERVER_URL = process.env.LEGION_AUTH_SERVER_URL || config.auth_server_url || 'https://auth.legion-ai.org';
43
+ const ACCESS_TOKEN = process.env.LEGION_ACCESS_TOKEN || config.access_token;
44
+
45
+ // --- API Client ---
46
+
47
+ async function legionRequest(path, options = {}) {
48
+ if (!ACCESS_TOKEN) {
49
+ throw new Error('LEGION_ACCESS_TOKEN not set. Please authenticate first.');
50
+ }
51
+
52
+ const url = `${AUTH_SERVER_URL}${path}`;
53
+ const response = await fetch(url, {
54
+ ...options,
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Authorization': `Bearer ${ACCESS_TOKEN}`,
58
+ ...options.headers,
59
+ },
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const error = await response.json().catch(() => ({ message: response.statusText }));
64
+ throw new Error(`Legion API error: ${error.message || response.statusText}`);
65
+ }
66
+
67
+ return response.json();
68
+ }
69
+
70
+ // --- Tool Implementations ---
71
+
72
+ async function listProjects() {
73
+ const data = await legionRequest('/v1/railway/projects');
74
+ return data.projects;
75
+ }
76
+
77
+ async function connectProject(projectName, projectId, environmentId, projectToken, serviceId) {
78
+ const data = await legionRequest('/v1/railway/connect', {
79
+ method: 'POST',
80
+ body: JSON.stringify({
81
+ project_name: projectName,
82
+ project_id: projectId,
83
+ environment_id: environmentId,
84
+ project_token: projectToken,
85
+ service_id: serviceId,
86
+ }),
87
+ });
88
+ return data.connection;
89
+ }
90
+
91
+ async function getLogs(connectionId, level, limit, filter) {
92
+ const params = new URLSearchParams();
93
+ if (level) params.set('level', level);
94
+ if (limit) params.set('limit', limit.toString());
95
+ if (filter) params.set('filter', filter);
96
+
97
+ const queryString = params.toString();
98
+ const path = `/v1/railway/logs/${connectionId}${queryString ? '?' + queryString : ''}`;
99
+
100
+ return legionRequest(path);
101
+ }
102
+
103
+ async function disconnectProject(connectionId) {
104
+ return legionRequest(`/v1/railway/disconnect/${connectionId}`, {
105
+ method: 'DELETE',
106
+ });
107
+ }
108
+
109
+ // --- MCP Server ---
110
+
111
+ const server = new Server(
112
+ {
113
+ name: 'legion',
114
+ version: '1.0.0',
115
+ },
116
+ {
117
+ capabilities: {
118
+ tools: {},
119
+ },
120
+ }
121
+ );
122
+
123
+ // List available tools
124
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
125
+ return {
126
+ tools: [
127
+ {
128
+ name: 'legion_list_projects',
129
+ description: 'List all projects connected to your Legion account for log access',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {},
133
+ required: [],
134
+ },
135
+ },
136
+ {
137
+ name: 'legion_connect_project',
138
+ description: 'Connect a project to your Legion account for deployment log access',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ project_name: {
143
+ type: 'string',
144
+ description: 'A friendly name for this project (e.g., "my-api")',
145
+ },
146
+ project_id: {
147
+ type: 'string',
148
+ description: 'Project ID from your hosting platform',
149
+ },
150
+ environment_id: {
151
+ type: 'string',
152
+ description: 'Environment ID (e.g., production, staging)',
153
+ },
154
+ project_token: {
155
+ type: 'string',
156
+ description: 'Project access token from your hosting platform settings',
157
+ },
158
+ service_id: {
159
+ type: 'string',
160
+ description: 'Optional: Service ID for multi-service projects',
161
+ },
162
+ },
163
+ required: ['project_name', 'project_id', 'environment_id', 'project_token'],
164
+ },
165
+ },
166
+ {
167
+ name: 'legion_get_logs',
168
+ description: 'Fetch deployment logs from a connected project. Use this to debug errors and monitor your application.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {
172
+ connection_id: {
173
+ type: 'string',
174
+ description: 'The connection ID returned when you connected the project',
175
+ },
176
+ level: {
177
+ type: 'string',
178
+ enum: ['error', 'warn', 'info'],
179
+ description: 'Filter logs by level. Use "error" to see only errors.',
180
+ },
181
+ limit: {
182
+ type: 'number',
183
+ description: 'Maximum number of log lines to return (default: 100, max: 500)',
184
+ },
185
+ filter: {
186
+ type: 'string',
187
+ description: 'Advanced filter syntax (e.g., "@level:error")',
188
+ },
189
+ },
190
+ required: ['connection_id'],
191
+ },
192
+ },
193
+ {
194
+ name: 'legion_get_errors',
195
+ description: 'Shortcut to fetch only error-level logs. Use this to quickly identify issues in your deployment.',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ connection_id: {
200
+ type: 'string',
201
+ description: 'The connection ID returned when you connected the project',
202
+ },
203
+ limit: {
204
+ type: 'number',
205
+ description: 'Maximum number of error lines to return (default: 50)',
206
+ },
207
+ },
208
+ required: ['connection_id'],
209
+ },
210
+ },
211
+ {
212
+ name: 'legion_disconnect_project',
213
+ description: 'Disconnect a project from your Legion account',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ connection_id: {
218
+ type: 'string',
219
+ description: 'The connection ID to disconnect',
220
+ },
221
+ },
222
+ required: ['connection_id'],
223
+ },
224
+ },
225
+ ],
226
+ };
227
+ });
228
+
229
+ // Handle tool calls
230
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
231
+ const { name, arguments: args } = request.params;
232
+
233
+ try {
234
+ let result;
235
+
236
+ switch (name) {
237
+ case 'legion_list_projects':
238
+ result = await listProjects();
239
+ break;
240
+
241
+ case 'legion_connect_project':
242
+ result = await connectProject(
243
+ args.project_name,
244
+ args.project_id,
245
+ args.environment_id,
246
+ args.project_token,
247
+ args.service_id
248
+ );
249
+ break;
250
+
251
+ case 'legion_get_logs':
252
+ result = await getLogs(
253
+ args.connection_id,
254
+ args.level,
255
+ args.limit,
256
+ args.filter
257
+ );
258
+ break;
259
+
260
+ case 'legion_get_errors':
261
+ result = await getLogs(
262
+ args.connection_id,
263
+ 'error',
264
+ args.limit || 50,
265
+ null
266
+ );
267
+ break;
268
+
269
+ case 'legion_disconnect_project':
270
+ result = await disconnectProject(args.connection_id);
271
+ break;
272
+
273
+ default:
274
+ throw new Error(`Unknown tool: ${name}`);
275
+ }
276
+
277
+ return {
278
+ content: [
279
+ {
280
+ type: 'text',
281
+ text: JSON.stringify(result, null, 2),
282
+ },
283
+ ],
284
+ };
285
+ } catch (error) {
286
+ return {
287
+ content: [
288
+ {
289
+ type: 'text',
290
+ text: `Error: ${error.message}`,
291
+ },
292
+ ],
293
+ isError: true,
294
+ };
295
+ }
296
+ });
297
+
298
+ // Start server
299
+ async function main() {
300
+ const transport = new StdioServerTransport();
301
+ await server.connect(transport);
302
+ console.error('[LEGION MCP] Server started');
303
+ }
304
+
305
+ main().catch((error) => {
306
+ console.error('[LEGION MCP] Fatal error:', error);
307
+ process.exit(1);
308
+ });