@latentforce/shift 1.0.0 → 1.0.2

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.
@@ -0,0 +1,334 @@
1
+ import WebSocket from 'ws';
2
+ import { EventEmitter } from 'events';
3
+ import { WS_URL } from '../utils/config.js';
4
+ import { ToolsExecutor } from './tools-executor.js';
5
+ const RECONNECT_INTERVAL = 5000; // 5 seconds - matching extension
6
+ const MAX_RECONNECT_ATTEMPTS = 100; // matching extension
7
+ /**
8
+ * WebSocket client matching extension's websocket-client.js
9
+ */
10
+ export class WebSocketClient extends EventEmitter {
11
+ ws = null;
12
+ authenticated = false;
13
+ projectInfo = null;
14
+ userInfo = null;
15
+ toolsExecutor = null;
16
+ shouldReconnect = true;
17
+ reconnectTimer = null;
18
+ apiKey;
19
+ projectId;
20
+ wsUrl;
21
+ isConnecting = false;
22
+ reconnectAttempts = 0;
23
+ maxReconnectAttempts = MAX_RECONNECT_ATTEMPTS;
24
+ workspaceRoot;
25
+ constructor(options) {
26
+ super();
27
+ this.apiKey = options.apiKey;
28
+ this.projectId = options.projectId;
29
+ this.wsUrl = options.wsUrl || WS_URL;
30
+ this.workspaceRoot = options.workspaceRoot || process.cwd();
31
+ // Initialize tools executor
32
+ this.toolsExecutor = new ToolsExecutor(this, this.workspaceRoot);
33
+ }
34
+ /**
35
+ * Set the tools executor
36
+ */
37
+ setToolsExecutor(toolsExecutor) {
38
+ this.toolsExecutor = toolsExecutor;
39
+ console.log('[WS-Client] Tools executor set');
40
+ }
41
+ /**
42
+ * Connect to WebSocket server
43
+ */
44
+ async connect() {
45
+ if (!this.apiKey || !this.projectId) {
46
+ throw new Error('API key and project ID are required');
47
+ }
48
+ this.shouldReconnect = true;
49
+ this.reconnectAttempts = 0;
50
+ console.log(`[WS-Client] Initiating connection to project: ${this.projectId}`);
51
+ return this._attemptConnection();
52
+ }
53
+ /**
54
+ * Internal method to attempt connection
55
+ */
56
+ async _attemptConnection() {
57
+ if (this.isConnecting) {
58
+ console.log('[WS-Client] Connection attempt already in progress, skipping...');
59
+ return;
60
+ }
61
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
62
+ console.error(`[WS-Client] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`);
63
+ this.emit('max_reconnects_reached');
64
+ this.shouldReconnect = false;
65
+ return;
66
+ }
67
+ this.isConnecting = true;
68
+ this.reconnectAttempts++;
69
+ // Emit reconnecting event
70
+ this.emit('reconnecting', this.reconnectAttempts);
71
+ return new Promise((resolve, reject) => {
72
+ // IMPORTANT: Use project_id in URL, matching extension
73
+ const wsUrl = `${this.wsUrl}/ws/extension/${this.projectId}`;
74
+ console.log(`[WS-Client] Connecting to ${wsUrl} (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
75
+ try {
76
+ this.ws = new WebSocket(wsUrl);
77
+ }
78
+ catch (error) {
79
+ console.error('[WS-Client] Failed to create WebSocket:', error);
80
+ this.isConnecting = false;
81
+ this._scheduleReconnect();
82
+ reject(error);
83
+ return;
84
+ }
85
+ // Connection timeout (10 seconds) - matching extension
86
+ const timeout = setTimeout(() => {
87
+ console.log('[WS-Client] Connection timeout (10s)');
88
+ if (this.ws) {
89
+ this.ws.close();
90
+ }
91
+ this.isConnecting = false;
92
+ this._scheduleReconnect();
93
+ reject(new Error('Connection timeout'));
94
+ }, 10000);
95
+ // WebSocket opened - send auth
96
+ this.ws.on('open', () => {
97
+ console.log('[WS-Client] ✓ WebSocket connection opened');
98
+ this.emit('connecting');
99
+ // Send authentication message - matching extension format
100
+ const authMessage = {
101
+ type: 'auth',
102
+ api_key: this.apiKey,
103
+ project_id: this.projectId,
104
+ };
105
+ console.log('[WS-Client] Sending authentication...');
106
+ this.ws.send(JSON.stringify(authMessage));
107
+ });
108
+ // Message received
109
+ this.ws.on('message', (data) => {
110
+ try {
111
+ const message = JSON.parse(data.toString());
112
+ if (message.type === 'auth_success') {
113
+ clearTimeout(timeout);
114
+ this.authenticated = true;
115
+ this.userInfo = message.user;
116
+ this.projectInfo = message.project;
117
+ this.isConnecting = false;
118
+ this.reconnectAttempts = 0;
119
+ console.log('[WS-Client] ✓✓✓ AUTHENTICATION SUCCESSFUL ✓✓✓');
120
+ if (this.userInfo) {
121
+ console.log(`[WS-Client] User: ${this.userInfo.email}`);
122
+ }
123
+ if (this.projectInfo) {
124
+ console.log(`[WS-Client] Project: ${this.projectInfo.project_name}`);
125
+ }
126
+ if (this.reconnectTimer) {
127
+ clearTimeout(this.reconnectTimer);
128
+ this.reconnectTimer = null;
129
+ }
130
+ // Emit connected event
131
+ this.emit('connected', this.projectInfo);
132
+ resolve(message);
133
+ }
134
+ else if (message.type === 'auth_failed') {
135
+ clearTimeout(timeout);
136
+ console.error('[WS-Client] ❌ AUTHENTICATION FAILED:', message.message);
137
+ this.isConnecting = false;
138
+ this.shouldReconnect = false; // Don't retry on auth failure
139
+ this.emit('auth_failed', message.message);
140
+ reject(new Error(message.message));
141
+ }
142
+ else {
143
+ // Handle other messages
144
+ this.handleMessage(message);
145
+ }
146
+ }
147
+ catch (err) {
148
+ console.error('[WS-Client] Error parsing message:', err);
149
+ }
150
+ });
151
+ // WebSocket error
152
+ this.ws.on('error', (error) => {
153
+ clearTimeout(timeout);
154
+ console.error('[WS-Client] WebSocket error:', error.message);
155
+ this.isConnecting = false;
156
+ this.emit('error', error);
157
+ this._scheduleReconnect();
158
+ reject(error);
159
+ });
160
+ // WebSocket closed
161
+ this.ws.on('close', (code, reason) => {
162
+ clearTimeout(timeout);
163
+ console.log(`[WS-Client] Connection closed (code: ${code}, reason: ${reason?.toString() || 'none'})`);
164
+ this.authenticated = false;
165
+ this.isConnecting = false;
166
+ this.emit('disconnected', code, reason?.toString());
167
+ if (this.shouldReconnect) {
168
+ this._scheduleReconnect();
169
+ }
170
+ });
171
+ });
172
+ }
173
+ /**
174
+ * Schedule reconnection attempt
175
+ */
176
+ _scheduleReconnect() {
177
+ if (this.reconnectTimer || !this.shouldReconnect) {
178
+ return;
179
+ }
180
+ console.log(`[WS-Client] Scheduling reconnect in ${RECONNECT_INTERVAL}ms... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
181
+ this.reconnectTimer = setTimeout(() => {
182
+ this.reconnectTimer = null;
183
+ console.log('[WS-Client] Attempting to reconnect...');
184
+ this._attemptConnection().catch(err => {
185
+ console.error('[WS-Client] Reconnection failed:', err.message);
186
+ });
187
+ }, RECONNECT_INTERVAL);
188
+ }
189
+ /**
190
+ * Handle incoming messages
191
+ */
192
+ async handleMessage(message) {
193
+ console.log(`[WS-Client] ← Received: ${message.type}`);
194
+ if (message.type === 'execute_tool') {
195
+ await this.handleToolRequest(message);
196
+ }
197
+ else if (message.type === 'tool_ack') {
198
+ console.log(`[WS-Client] Server acknowledged tool: ${message.tool} (${message.request_id})`);
199
+ this.emit('tool_acknowledged', message);
200
+ }
201
+ else if (message.type === 'pong') {
202
+ console.log('[WS-Client] Pong received');
203
+ this.emit('pong');
204
+ }
205
+ else if (message.type === 'error') {
206
+ console.error('[WS-Client] Server error:', message.message);
207
+ this.emit('server_error', message.message);
208
+ }
209
+ else if (message.type === 'user_input_required') {
210
+ console.log('[WS-Client] 🔔 User input required notification received');
211
+ console.log(`[WS-Client] Agent: ${message.agent_name}`);
212
+ console.log(`[WS-Client] Questions: ${message.questions_count}`);
213
+ this.emit('user_input_required', {
214
+ agent_id: message.agent_id,
215
+ agent_name: message.agent_name,
216
+ questions_count: message.questions_count,
217
+ message: message.message
218
+ });
219
+ }
220
+ else {
221
+ console.log('[WS-Client] Unknown message type:', message.type);
222
+ this.emit('message', message);
223
+ }
224
+ }
225
+ /**
226
+ * Handle tool execution request from server
227
+ */
228
+ async handleToolRequest(message) {
229
+ const toolName = message.tool;
230
+ const params = message.params || {};
231
+ const requestId = message.request_id;
232
+ console.log(`[WS-Client] ═══════════════════════════════════`);
233
+ console.log(`[WS-Client] TOOL EXECUTION REQUEST`);
234
+ console.log(`[WS-Client] Tool: ${toolName}`);
235
+ console.log(`[WS-Client] Request ID: ${requestId}`);
236
+ console.log(`[WS-Client] Params:`, JSON.stringify(params, null, 2));
237
+ console.log(`[WS-Client] ═══════════════════════════════════`);
238
+ if (!this.toolsExecutor) {
239
+ console.error('[WS-Client] ❌ Tools executor not available');
240
+ await this.sendMessage({
241
+ type: 'tool_error',
242
+ tool: toolName,
243
+ request_id: requestId,
244
+ error: 'Tools executor not initialized'
245
+ });
246
+ return;
247
+ }
248
+ try {
249
+ console.log(`[WS-Client] Executing tool: ${toolName}...`);
250
+ const result = await this.toolsExecutor.executeTool(toolName, params);
251
+ console.log(`[WS-Client] ✓ Tool execution completed`);
252
+ console.log(`[WS-Client] Result status: ${result?.status || 'unknown'}`);
253
+ await this.sendMessage({
254
+ type: 'tool_result',
255
+ tool: toolName,
256
+ result: result,
257
+ request_id: requestId,
258
+ timestamp: new Date().toISOString()
259
+ });
260
+ console.log(`[WS-Client] → Tool result sent to server`);
261
+ this.emit('tool_executed', { tool: toolName, result, requestId });
262
+ }
263
+ catch (error) {
264
+ console.error(`[WS-Client] ❌ Tool execution failed:`, error);
265
+ await this.sendMessage({
266
+ type: 'tool_error',
267
+ tool: toolName,
268
+ request_id: requestId,
269
+ error: error.message,
270
+ timestamp: new Date().toISOString()
271
+ });
272
+ this.emit('tool_error', { tool: toolName, error, requestId });
273
+ }
274
+ }
275
+ /**
276
+ * Send a message to the server
277
+ */
278
+ async sendMessage(message) {
279
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
280
+ throw new Error('WebSocket not connected');
281
+ }
282
+ this.ws.send(JSON.stringify(message));
283
+ }
284
+ /**
285
+ * Send a ping to keep connection alive
286
+ */
287
+ async ping() {
288
+ if (this.isConnected()) {
289
+ await this.sendMessage({ type: 'ping' });
290
+ console.log('[WS-Client] → Sent: ping');
291
+ }
292
+ }
293
+ /**
294
+ * Check if connected and authenticated
295
+ */
296
+ isConnected() {
297
+ return this.authenticated && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
298
+ }
299
+ /**
300
+ * Disconnect from server
301
+ */
302
+ disconnect() {
303
+ console.log('[WS-Client] Manual disconnect requested');
304
+ this.shouldReconnect = false;
305
+ if (this.reconnectTimer) {
306
+ clearTimeout(this.reconnectTimer);
307
+ this.reconnectTimer = null;
308
+ }
309
+ if (this.ws) {
310
+ this.ws.close();
311
+ this.ws = null;
312
+ }
313
+ this.authenticated = false;
314
+ this.emit('disconnected', 1000, 'Manual disconnect');
315
+ }
316
+ /**
317
+ * Get project information
318
+ */
319
+ getProjectInfo() {
320
+ return this.projectInfo;
321
+ }
322
+ /**
323
+ * Get user information
324
+ */
325
+ getUserInfo() {
326
+ return this.userInfo;
327
+ }
328
+ /**
329
+ * Get current reconnection attempt count
330
+ */
331
+ getReconnectAttempts() {
332
+ return this.reconnectAttempts;
333
+ }
334
+ }
package/build/index.js CHANGED
@@ -1,132 +1,45 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from 'zod';
5
- // Create server instance
6
- const server = new McpServer({
7
- name: "shift",
8
- version: "1.0.0",
2
+ import { Command } from 'commander';
3
+ const program = new Command();
4
+ program
5
+ .name('shift')
6
+ .description('Shift CLI - AI-powered code intelligence')
7
+ .version('1.0.2');
8
+ // MCP server mode (default when run via MCP host)
9
+ program
10
+ .command('mcp', { isDefault: true, hidden: true })
11
+ .description('Start MCP server on stdio')
12
+ .action(async () => {
13
+ const { startMcpServer } = await import('./mcp-server.js');
14
+ await startMcpServer();
9
15
  });
10
- const BASE_URL = process.env.SHIFT_BACKEND_URL || "http://127.0.0.1:9000";
11
- function getProjectIdFromEnv() {
12
- const projectId = process.env.SHIFT_PROJECT_ID;
13
- if (!projectId || projectId.trim() === "") {
14
- throw new Error("SHIFT_PROJECT_ID environment variable is not set. " +
15
- "Set it to your Shift Lite project UUID, or pass project_id in each tool call.");
16
- }
17
- return projectId.trim();
18
- }
19
- /** Resolve project_id: use tool arg if provided, else fall back to env. */
20
- function resolveProjectId(args) {
21
- const fromArgs = args.project_id?.trim();
22
- if (fromArgs)
23
- return fromArgs;
24
- return getProjectIdFromEnv();
25
- }
26
- // helper
27
- async function callBackendAPI(endpoint, data) {
28
- try {
29
- const response = await fetch(`${BASE_URL}${endpoint}`, {
30
- method: 'POST',
31
- headers: {
32
- 'Content-Type': 'application/json',
33
- },
34
- body: JSON.stringify(data),
35
- });
36
- if (!response.ok) {
37
- const text = await response.text();
38
- throw new Error(`API call failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
39
- }
40
- return await response.json();
41
- }
42
- catch (error) {
43
- console.error(`Error calling ${endpoint}:`, error);
44
- throw error;
45
- }
46
- }
47
- // Tools:
48
- // Blast radius
49
- server.registerTool("blast_radius", {
50
- description: "Analyzes the blast radius of a file or component - shows what would be affected if this file were modified or deleted",
51
- inputSchema: z.object({
52
- file_path: z.string().describe("Path to the file (relative to project root)"),
53
- project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
54
- project_path: z.string().optional().describe("Path to the root of the project (optional)"),
55
- level: z.number().optional().describe("Max depth of blast radius (optional)"),
56
- })
57
- }, async (args) => {
58
- const projectId = resolveProjectId(args);
59
- const data = await callBackendAPI('/api/v1/mcp/blast-radius', {
60
- path: args.file_path,
61
- project_id: projectId,
62
- ...(args.level != null && { level: args.level }),
63
- });
64
- return {
65
- content: [
66
- {
67
- type: "text",
68
- text: JSON.stringify(data, null, 2)
69
- },
70
- ],
71
- };
16
+ // CLI commands
17
+ program
18
+ .command('start')
19
+ .description('Start the Shift daemon for this project')
20
+ .action(async () => {
21
+ const { startCommand } = await import('./cli/commands/start.js');
22
+ await startCommand();
72
23
  });
73
- // Dependencies
74
- server.registerTool("dependencies", {
75
- description: "Retrieves all dependencies for a given file or component, including direct and transitive dependencies",
76
- inputSchema: z.object({
77
- file_path: z.string().describe("Path to the file"),
78
- project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
79
- project_path: z.string().optional().describe("Path to the root of the project (optional)"),
80
- })
81
- }, async (args) => {
82
- const projectId = resolveProjectId(args);
83
- const data = await callBackendAPI('/api/v1/mcp/dependency', {
84
- path: args.file_path,
85
- project_id: projectId,
86
- });
87
- return {
88
- content: [
89
- {
90
- type: "text",
91
- text: JSON.stringify(data, null, 2)
92
- },
93
- ],
94
- };
24
+ program
25
+ .command('init')
26
+ .description('Initialize and scan the project for file indexing')
27
+ .action(async () => {
28
+ const { initCommand } = await import('./cli/commands/init.js');
29
+ await initCommand();
95
30
  });
96
- // File summary (maps to what-is-this-file)
97
- server.registerTool("file_summary", {
98
- description: "Generates a comprehensive summary of a file including its purpose, exports, imports, and key functions, with optional parent directory context",
99
- inputSchema: z.object({
100
- file_path: z.string().describe("Path to the file to summarize"),
101
- project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
102
- project_path: z.string().optional().describe("Path to the root of the project (optional)"),
103
- level: z.number().optional().describe("Number of parent directory levels to include (default 0)"),
104
- })
105
- }, async (args) => {
106
- const projectId = resolveProjectId(args);
107
- const data = await callBackendAPI('/api/v1/mcp/what-is-this-file', {
108
- path: args.file_path,
109
- project_id: projectId,
110
- level: args.level ?? 0,
111
- });
112
- return {
113
- content: [
114
- {
115
- type: "text",
116
- text: JSON.stringify(data, null, 2)
117
- },
118
- ],
119
- };
31
+ program
32
+ .command('stop')
33
+ .description('Stop the Shift daemon')
34
+ .action(async () => {
35
+ const { stopCommand } = await import('./cli/commands/stop.js');
36
+ await stopCommand();
120
37
  });
121
- async function main() {
122
- const transport = new StdioServerTransport();
123
- await server.connect(transport);
124
- console.error("Shift MCP Server running on stdio");
125
- if (!process.env.SHIFT_PROJECT_ID) {
126
- console.error("Warning: SHIFT_PROJECT_ID is not set. Pass project_id in each tool call, or set the env var.");
127
- }
128
- }
129
- main().catch((error) => {
130
- console.error("Fatal error in main():", error);
131
- process.exit(1);
38
+ program
39
+ .command('status')
40
+ .description('Show the current Shift status')
41
+ .action(async () => {
42
+ const { statusCommand } = await import('./cli/commands/status.js');
43
+ await statusCommand();
132
44
  });
45
+ program.parse();
@@ -0,0 +1,124 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from 'zod';
4
+ const BASE_URL = process.env.SHIFT_BACKEND_URL || "http://127.0.0.1:9000";
5
+ function getProjectIdFromEnv() {
6
+ const projectId = process.env.SHIFT_PROJECT_ID;
7
+ if (!projectId || projectId.trim() === "") {
8
+ throw new Error("SHIFT_PROJECT_ID environment variable is not set. " +
9
+ "Set it to your Shift Lite project UUID, or pass project_id in each tool call.");
10
+ }
11
+ return projectId.trim();
12
+ }
13
+ /** Resolve project_id: use tool arg if provided, else fall back to env. */
14
+ function resolveProjectId(args) {
15
+ const fromArgs = args.project_id?.trim();
16
+ if (fromArgs)
17
+ return fromArgs;
18
+ return getProjectIdFromEnv();
19
+ }
20
+ // helper
21
+ async function callBackendAPI(endpoint, data) {
22
+ try {
23
+ const response = await fetch(`${BASE_URL}${endpoint}`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ body: JSON.stringify(data),
29
+ });
30
+ if (!response.ok) {
31
+ const text = await response.text();
32
+ throw new Error(`API call failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
33
+ }
34
+ return await response.json();
35
+ }
36
+ catch (error) {
37
+ console.error(`Error calling ${endpoint}:`, error);
38
+ throw error;
39
+ }
40
+ }
41
+ export async function startMcpServer() {
42
+ // Create server instance
43
+ const server = new McpServer({
44
+ name: "shift",
45
+ version: "1.0.2",
46
+ });
47
+ // Tools:
48
+ // Blast radius
49
+ server.registerTool("blast_radius", {
50
+ description: "Analyzes the blast radius of a file or component - shows what would be affected if this file were modified or deleted",
51
+ inputSchema: z.object({
52
+ file_path: z.string().describe("Path to the file (relative to project root)"),
53
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
54
+ level: z.number().optional().describe("Max depth of blast radius (optional)"),
55
+ })
56
+ }, async (args) => {
57
+ const projectId = resolveProjectId(args);
58
+ const data = await callBackendAPI('/api/v1/mcp/blast-radius', {
59
+ path: args.file_path,
60
+ project_id: projectId,
61
+ ...(args.level != null && { level: args.level }),
62
+ });
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify(data, null, 2)
68
+ },
69
+ ],
70
+ };
71
+ });
72
+ // Dependencies
73
+ server.registerTool("dependencies", {
74
+ description: "Retrieves all dependencies for a given file or component, including direct and transitive dependencies",
75
+ inputSchema: z.object({
76
+ file_path: z.string().describe("Path to the file"),
77
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
78
+ })
79
+ }, async (args) => {
80
+ const projectId = resolveProjectId(args);
81
+ const data = await callBackendAPI('/api/v1/mcp/dependency', {
82
+ path: args.file_path,
83
+ project_id: projectId,
84
+ });
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: JSON.stringify(data, null, 2)
90
+ },
91
+ ],
92
+ };
93
+ });
94
+ // File summary (maps to what-is-this-file)
95
+ server.registerTool("file_summary", {
96
+ description: "Generates a comprehensive summary of a file including its purpose, exports, imports, and key functions, with optional parent directory context",
97
+ inputSchema: z.object({
98
+ file_path: z.string().describe("Path to the file to summarize"),
99
+ project_id: z.string().optional().describe("Shift Lite project UUID; overrides SHIFT_PROJECT_ID if provided"),
100
+ level: z.number().optional().describe("Number of parent directory levels to include (default 0)"),
101
+ })
102
+ }, async (args) => {
103
+ const projectId = resolveProjectId(args);
104
+ const data = await callBackendAPI('/api/v1/mcp/what-is-this-file', {
105
+ path: args.file_path,
106
+ project_id: projectId,
107
+ level: args.level ?? 0,
108
+ });
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: JSON.stringify(data, null, 2)
114
+ },
115
+ ],
116
+ };
117
+ });
118
+ const transport = new StdioServerTransport();
119
+ await server.connect(transport);
120
+ console.error("Shift MCP Server running on stdio");
121
+ if (!process.env.SHIFT_PROJECT_ID) {
122
+ console.error("Warning: SHIFT_PROJECT_ID is not set. Pass project_id in each tool call, or set the env var.");
123
+ }
124
+ }
@@ -0,0 +1,50 @@
1
+ import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
2
+ /**
3
+ * Fetch available projects for the user
4
+ * Matching extension's fetchProjects function in api-client.js
5
+ */
6
+ export async function fetchProjects(apiKey) {
7
+ try {
8
+ const response = await fetch(`${API_BASE_URL}/api/vscode-projects`, {
9
+ method: 'GET',
10
+ headers: {
11
+ 'Authorization': `Bearer ${apiKey}`,
12
+ },
13
+ });
14
+ if (!response.ok) {
15
+ const text = await response.text();
16
+ throw new Error(text || `HTTP ${response.status}`);
17
+ }
18
+ const data = await response.json();
19
+ return data.projects || [];
20
+ }
21
+ catch (error) {
22
+ console.error('Failed to fetch projects:', error);
23
+ throw new Error(error.message || 'Failed to fetch projects');
24
+ }
25
+ }
26
+ /**
27
+ * Send init scan to backend
28
+ * Matching extension's init-scan API call
29
+ */
30
+ export async function sendInitScan(apiKey, projectId, payload) {
31
+ try {
32
+ const response = await fetch(`${API_BASE_URL_ORCH}/api/projects/${projectId}/init-scan`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Authorization': `Bearer ${apiKey}`,
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify(payload),
39
+ });
40
+ if (!response.ok) {
41
+ const text = await response.text();
42
+ throw new Error(text || `HTTP ${response.status}`);
43
+ }
44
+ return await response.json();
45
+ }
46
+ catch (error) {
47
+ console.error('Failed to send init scan:', error);
48
+ throw error;
49
+ }
50
+ }