Skip to content

Commit 7d14768

Browse files
authored
Merge pull request #834 from Lemoncode/refactor/vscode-extension-mcp-restructure
Refactor/vscode-extension-mcp-restructure
2 parents 61357bd + 87c2fb2 commit 7d14768

28 files changed

Lines changed: 357 additions & 415 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './new-wireframe';
2+
export * from './register';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as vscode from 'vscode';
2+
3+
export const registerNewWireframeCommand = (
4+
context: vscode.ExtensionContext
5+
): void => {
6+
context.subscriptions.push(
7+
vscode.commands.registerCommand('quickmock.newWireframe', () => {
8+
vscode.window.showInformationMessage('New wireframe coming soon'); // TODO: Implement the actual functionality for creating a new wireframe
9+
})
10+
);
11+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as vscode from 'vscode';
2+
import { registerNewWireframeCommand } from './new-wireframe';
3+
4+
/**
5+
* Registers all VS Code commands exposed by the extension.
6+
* @param context The VS Code extension context.
7+
*/
8+
export const registerCommands = (context: vscode.ExtensionContext): void => {
9+
registerNewWireframeCommand(context);
10+
};

packages/vscode-extension/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './config';
22
export * from './document-registry';
33
export * from './logger';
44
export * from './paths';
5+
export * from './workspace';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as vscode from 'vscode';
2+
3+
export const getPrimaryWorkspaceFolder = ():
4+
| vscode.WorkspaceFolder
5+
| undefined => vscode.workspace.workspaceFolders?.[0];
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
1-
import { logError, onAppUrlChange, syncAppUrlFile } from '#core';
1+
import { registerCommands } from '#commands';
2+
import { onAppUrlChange, syncAppUrlFile } from '#core';
23
import { QuickMockEditorProvider } from '#editor';
3-
import {
4-
registerMcpServer,
5-
registerQuickMockMcpServerProvider,
6-
RegistryServer,
7-
} from '#mcp';
4+
import { setupMcp } from '#mcp';
85
import * as vscode from 'vscode';
96

107
export const activate = (context: vscode.ExtensionContext) => {
118
syncAppUrlFile();
129
context.subscriptions.push(onAppUrlChange(syncAppUrlFile));
13-
1410
context.subscriptions.push(QuickMockEditorProvider.register(context));
15-
16-
const registryServer = new RegistryServer();
17-
registryServer
18-
.start(context)
19-
.catch(err => logError('Failed to start MCP registry server:', err));
20-
21-
context.subscriptions.push(registerQuickMockMcpServerProvider(context));
22-
23-
registerMcpServer(context).catch(err =>
24-
logError('Failed to register MCP server:', err)
25-
);
26-
27-
context.subscriptions.push(
28-
vscode.commands.registerCommand('quickmock.newWireframe', () => {
29-
vscode.window.showInformationMessage('New wireframe coming soon');
30-
})
31-
);
11+
setupMcp(context);
12+
registerCommands(context);
3213
};
3314

3415
export const deactivate = () => {};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const TOKEN_BYTE_LENGTH = 32;
2+
export const PORT_FILE_MODE = 0o600;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './server';
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { documentRegistry, getPrimaryWorkspaceFolder } from '#core';
2+
import {
3+
buildPortFilePath,
4+
DOCUMENT_ROUTE,
5+
encodePortFile,
6+
LOOPBACK_HOST,
7+
TOKEN_HEADER,
8+
} from '@lemoncode/quickmock-registry-protocol';
9+
import { randomBytes } from 'node:crypto';
10+
import { unlinkSync, writeFileSync } from 'node:fs';
11+
import {
12+
createServer,
13+
type IncomingMessage,
14+
type ServerResponse,
15+
} from 'node:http';
16+
import * as vscode from 'vscode';
17+
import { PORT_FILE_MODE, TOKEN_BYTE_LENGTH } from './constants';
18+
19+
/**
20+
* Starts the MCP document bridge server, which serves the content of documents open in the editor to the MCP server.
21+
* @param context The VS Code extension context.
22+
* @returns A promise that resolves when the server has started.
23+
*/
24+
export const startDocumentBridge = async (
25+
context: vscode.ExtensionContext
26+
): Promise<void> => {
27+
const workspaceRoot = getPrimaryWorkspaceFolder()?.uri.fsPath;
28+
if (!workspaceRoot) return;
29+
30+
const portFile = buildPortFilePath(workspaceRoot);
31+
const token = randomBytes(TOKEN_BYTE_LENGTH).toString('hex');
32+
33+
const handleRequest = (req: IncomingMessage, res: ServerResponse): void => {
34+
if (req.headers[TOKEN_HEADER] !== token) {
35+
res.writeHead(401);
36+
res.end();
37+
return;
38+
}
39+
40+
const url = new URL(req.url ?? '/', 'http://localhost');
41+
42+
if (url.pathname !== DOCUMENT_ROUTE) {
43+
res.writeHead(404);
44+
res.end();
45+
return;
46+
}
47+
48+
const path = url.searchParams.get('path');
49+
if (!path) {
50+
res.writeHead(400);
51+
res.end('Missing path parameter');
52+
return;
53+
}
54+
55+
const content = documentRegistry.get(path);
56+
if (content === undefined) {
57+
res.writeHead(404);
58+
res.end('Document not open in editor');
59+
return;
60+
}
61+
62+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
63+
res.end(content);
64+
};
65+
66+
const server = createServer(handleRequest);
67+
68+
await new Promise<void>((resolve, reject) => {
69+
server.on('error', reject);
70+
// Get the assigned port and write it to the port file
71+
server.listen(0, LOOPBACK_HOST, () => {
72+
const { port } = server.address() as { port: number };
73+
try {
74+
writeFileSync(portFile, encodePortFile(port, token), {
75+
mode: PORT_FILE_MODE,
76+
});
77+
} catch (err) {
78+
reject(err);
79+
return;
80+
}
81+
resolve();
82+
});
83+
});
84+
85+
context.subscriptions.push({
86+
dispose: () => {
87+
server.closeAllConnections();
88+
server.close();
89+
try {
90+
unlinkSync(portFile);
91+
} catch {}
92+
},
93+
});
94+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { homedir } from 'node:os';
2+
import { join } from 'node:path';
3+
import type { ExternalMcpClient } from '../model';
4+
5+
export const claudeCode: ExternalMcpClient = {
6+
label: 'Claude Code',
7+
getConfigPath: () => join(homedir(), '.claude.json'),
8+
};

0 commit comments

Comments
 (0)