All checks were successful
CI/CD / lint (pull_request) Successful in 1m40s
CI/CD / typecheck (pull_request) Successful in 1m35s
CI/CD / test (pull_request) Successful in 2m16s
CI/CD / build (pull_request) Successful in 2m17s
CI/CD / smoke (pull_request) Successful in 4m37s
CI/CD / publish (pull_request) Has been skipped
- status.ts + api-client.ts now dispatch on URL scheme so an https mcpd URL no longer crashes with "Protocol https: not supported". Caught by fulldeploy smoke runs — status.ts had `import http` only and was synchronously throwing against https://mcpctl.ad.itaz.eu. Each http.get call is wrapped so future scheme-mismatch errors also degrade to "unreachable" instead of a stack trace. - .dockerignore no longer excludes src/mcplocal/ (the new Dockerfile.mcplocal needs those files). - scripts/demo-mcp-call.py: standalone, stdlib-only Python demo that makes an MCP request (initialize + tools/list, optional tools/call) using an mcpctl_pat_ bearer. Counterpart to `mcpctl test mcp` for showing external (e.g. vLLM) clients how the bearer flow works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import http from 'node:http';
|
|
import https from 'node:https';
|
|
|
|
export interface ApiClientOptions {
|
|
baseUrl: string;
|
|
timeout?: number | undefined;
|
|
token?: string | undefined;
|
|
}
|
|
|
|
export interface ApiResponse<T = unknown> {
|
|
status: number;
|
|
data: T;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public readonly status: number,
|
|
public readonly body: string,
|
|
) {
|
|
super(`API error ${status}: ${body}`);
|
|
this.name = 'ApiError';
|
|
}
|
|
}
|
|
|
|
function request<T>(method: string, url: string, timeout: number, body?: unknown, token?: string): Promise<ApiResponse<T>> {
|
|
return new Promise((resolve, reject) => {
|
|
const parsed = new URL(url);
|
|
const headers: Record<string, string> = {};
|
|
if (body !== undefined) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
const isHttps = parsed.protocol === 'https:';
|
|
const opts: http.RequestOptions = {
|
|
hostname: parsed.hostname,
|
|
port: parsed.port || (isHttps ? 443 : 80),
|
|
path: parsed.pathname + parsed.search,
|
|
method,
|
|
timeout,
|
|
headers,
|
|
};
|
|
|
|
const driver = isHttps ? https : http;
|
|
const req = driver.request(opts, (res) => {
|
|
const chunks: Buffer[] = [];
|
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
const status = res.statusCode ?? 0;
|
|
if (status >= 400) {
|
|
reject(new ApiError(status, raw));
|
|
return;
|
|
}
|
|
try {
|
|
resolve({ status, data: JSON.parse(raw) as T });
|
|
} catch {
|
|
resolve({ status, data: raw as unknown as T });
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
reject(new Error(`Request to ${url} timed out`));
|
|
});
|
|
if (body !== undefined) {
|
|
req.write(JSON.stringify(body));
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
export class ApiClient {
|
|
private baseUrl: string;
|
|
private timeout: number;
|
|
private token?: string | undefined;
|
|
|
|
constructor(opts: ApiClientOptions) {
|
|
this.baseUrl = opts.baseUrl.replace(/\/$/, '');
|
|
this.timeout = opts.timeout ?? 10000;
|
|
this.token = opts.token;
|
|
}
|
|
|
|
async get<T = unknown>(path: string): Promise<T> {
|
|
const res = await request<T>('GET', `${this.baseUrl}${path}`, this.timeout, undefined, this.token);
|
|
return res.data;
|
|
}
|
|
|
|
async post<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
const res = await request<T>('POST', `${this.baseUrl}${path}`, this.timeout, body, this.token);
|
|
return res.data;
|
|
}
|
|
|
|
async put<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
const res = await request<T>('PUT', `${this.baseUrl}${path}`, this.timeout, body, this.token);
|
|
return res.data;
|
|
}
|
|
|
|
async delete(path: string): Promise<void> {
|
|
await request('DELETE', `${this.baseUrl}${path}`, this.timeout, undefined, this.token);
|
|
}
|
|
}
|