Skip to content

Commit 84c1797

Browse files
committed
feat: implement quickmock registry protocol and integrate with mcp and vscode extension
1 parent bb0a432 commit 84c1797

12 files changed

Lines changed: 145 additions & 37 deletions

File tree

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"devDependencies": {
3333
"@lemoncode/quickmock-bridge-protocol": "*",
34+
"@lemoncode/quickmock-registry-protocol": "*",
3435
"@lemoncode/typescript-config": "*",
3536
"@lemoncode/tsdown-config": "*",
3637
"@lemoncode/vitest-config": "*",

packages/mcp/src/core/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from './registry.client';
22
export * from './registry.models';
3-
export * from './registry.utils';

packages/mcp/src/core/registry.client.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
11
import { readFileSync } from 'node:fs';
2-
import { tmpdir } from 'node:os';
3-
import { join } from 'node:path';
2+
import {
3+
buildPortFilePath,
4+
DOCUMENT_ROUTE,
5+
LOOPBACK_HOST,
6+
parsePortFile,
7+
TOKEN_HEADER,
8+
} from '@lemoncode/quickmock-registry-protocol';
49
import { nullClient, type RegistryClient } from './registry.models';
5-
import { workspaceHash } from './registry.utils';
610

7-
/** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */
8-
export function createRegistryClient(): RegistryClient {
9-
const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd();
11+
const REQUEST_TIMEOUT_MS = 2_000;
1012

11-
let port: number;
13+
function readPortFile(workspaceRoot: string) {
1214
try {
13-
const hash = workspaceHash(workspaceRoot);
14-
const portFile = join(tmpdir(), `quickmock-${hash}.port`);
15-
port = parseInt(readFileSync(portFile, 'utf-8').trim(), 10);
16-
if (Number.isNaN(port)) {
17-
return nullClient;
18-
}
15+
const raw = readFileSync(buildPortFilePath(workspaceRoot), 'utf-8');
16+
return parsePortFile(raw);
1917
} catch {
20-
return nullClient;
18+
return null;
2119
}
20+
}
21+
22+
/** HTTP client for the VSCode extension's registry server. Falls back to nullClient when the extension is not running. */
23+
export function createRegistryClient(): RegistryClient {
24+
const workspaceRoot = process.env.QM_WORKSPACE_ROOT ?? process.cwd();
25+
const portFile = readPortFile(workspaceRoot);
26+
if (!portFile) return nullClient;
27+
const { port, token } = portFile;
2228

2329
return {
2430
async getDocument(fsPath: string): Promise<string | null> {
2531
try {
26-
const url = `http://127.0.0.1:${port}/document?path=${encodeURIComponent(fsPath)}`;
27-
const res = await fetch(url, { signal: AbortSignal.timeout(2_000) });
28-
if (!res.ok) {
29-
return null;
30-
}
32+
const url = `http://${LOOPBACK_HOST}:${port}${DOCUMENT_ROUTE}?path=${encodeURIComponent(fsPath)}`;
33+
const res = await fetch(url, {
34+
headers: { [TOKEN_HEADER]: token },
35+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
36+
});
37+
if (!res.ok) return null;
3138
return await res.text();
3239
} catch {
3340
return null;

packages/mcp/src/core/registry.utils.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@lemoncode/quickmock-registry-protocol",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"exports": {
7+
".": "./src/index.ts"
8+
},
9+
"scripts": {
10+
"check-types": "tsc --noEmit"
11+
},
12+
"devDependencies": {
13+
"@lemoncode/typescript-config": "*",
14+
"@types/node": "^22.19.17"
15+
}
16+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const TOKEN_HEADER = 'x-quickmock-token';
2+
export const LOOPBACK_HOST = '127.0.0.1';
3+
export const DOCUMENT_ROUTE = '/document';
4+
export const PORT_TOKEN_SEPARATOR = ' ';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './consts';
2+
export * from './utils';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createHash } from 'node:crypto';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
import { PORT_TOKEN_SEPARATOR } from './consts';
5+
6+
const WORKSPACE_HASH_LENGTH = 8;
7+
8+
export function buildPortFilePath(workspaceRoot: string): string {
9+
const hash = createHash('md5')
10+
.update(workspaceRoot)
11+
.digest('hex')
12+
.slice(0, WORKSPACE_HASH_LENGTH);
13+
return join(tmpdir(), `quickmock-${hash}.port`);
14+
}
15+
16+
export function encodePortFile(port: number, token: string): string {
17+
return `${port}${PORT_TOKEN_SEPARATOR}${token}`;
18+
}
19+
20+
export function parsePortFile(
21+
raw: string
22+
): { port: number; token: string } | null {
23+
const [portStr, token] = raw.trim().split(PORT_TOKEN_SEPARATOR);
24+
const port = parseInt(portStr ?? '', 10);
25+
if (Number.isNaN(port) || !token) return null;
26+
return { port, token };
27+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "@lemoncode/typescript-config/base",
3+
"include": ["src"],
4+
"compilerOptions": {
5+
"target": "ES2024",
6+
"types": ["node"],
7+
"lib": ["ES2024"],
8+
"noEmit": true,
9+
"rootDir": "src"
10+
}
11+
}

0 commit comments

Comments
 (0)