204 lines
8.6 KiB
TypeScript
204 lines
8.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { parseGenerateFlags } from '../src/cli/generate/flags.js';
|
|
import { inferNameFromCommand } from '../src/cli/generate/name-utils.js';
|
|
import { normalizeDefinition } from '../src/cli/generate/definition.js';
|
|
import { buildGenerateCliCommand } from '../src/cli/generate/template-data.js';
|
|
import { printGenerateCliHelp } from '../src/cli/generate-cli-runner.js';
|
|
import { serializeDefinition } from '../src/cli-metadata.js';
|
|
import type { SerializedServerDefinition } from '../src/cli-metadata.js';
|
|
|
|
describe('generate-cli runner internals', () => {
|
|
it('parses generate-cli flags including bundle/compile toggles', () => {
|
|
const args = [
|
|
'--server',
|
|
'linear',
|
|
'--command',
|
|
'https://example.com/mcp.getComponents()',
|
|
'--bundle',
|
|
'--compile',
|
|
'--minify',
|
|
];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.server).toBe('linear');
|
|
expect(parsed.command).toBe('https://example.com/mcp');
|
|
expect(parsed.bundle).toBe(true);
|
|
expect(parsed.compile).toBe(true);
|
|
expect(parsed.minify).toBe(true);
|
|
});
|
|
|
|
it('documents timeout and minify flags in generate-cli help', () => {
|
|
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
let output = '';
|
|
try {
|
|
printGenerateCliHelp();
|
|
output = String(spy.mock.calls[0]?.[0] ?? '');
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
expect(output).toContain('--timeout <ms>');
|
|
expect(output).toContain('--minify / --no-minify');
|
|
});
|
|
|
|
it('normalizes inferred names from URLs', () => {
|
|
const args = ['--command', 'https://api.linear.app/mcp.getComponents'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.command).toContain('https://');
|
|
const inferred = inferNameFromCommand(parsed.command ?? '');
|
|
expect(inferred).toBe('linear');
|
|
});
|
|
|
|
it('splits stdio commands and infers names from args', () => {
|
|
const args = ['--command', 'npx -y chrome-devtools-mcp@latest'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.command).toBeDefined();
|
|
expect(typeof parsed.command).toBe('object');
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.command).toBe('npx');
|
|
expect(spec.args).toEqual(['-y', 'chrome-devtools-mcp@latest']);
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toContain('chrome-devtools');
|
|
});
|
|
|
|
it('parses local script commands with extra args', () => {
|
|
const args = ['--command', 'bun run ./servers/local-cli.ts --stdio --name local'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.command).toBe('bun');
|
|
expect(spec.args).toEqual(['run', './servers/local-cli.ts', '--stdio', '--name', 'local']);
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('local-cli');
|
|
});
|
|
|
|
it('infers package names from scoped arguments', () => {
|
|
const args = ['--command', 'npx -y @demo/tools@latest serve'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.args).toEqual(['-y', '@demo/tools@latest', 'serve']);
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('demo-tools');
|
|
});
|
|
|
|
it('infers npm package names without version specifiers in inline commands', () => {
|
|
const args = ['--command', 'npx -y chrome-devtools-mcp'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.args).toEqual(['-y', 'chrome-devtools-mcp']);
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('chrome-devtools-mcp');
|
|
});
|
|
|
|
it('normalizes scheme-less HTTP selectors passed to --command', () => {
|
|
const args = ['--command', 'shadcn.io/api/mcp.getComponents'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(typeof parsed.command).toBe('string');
|
|
expect((parsed.command as string).startsWith('https://')).toBe(true);
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('shadcn');
|
|
});
|
|
|
|
it('preserves HTTP fetch compatibility metadata in generated CLI definitions', () => {
|
|
const definition = normalizeDefinition({
|
|
name: 'sunsama',
|
|
command: 'https://api.sunsama.com/mcp',
|
|
httpFetch: 'node-http1',
|
|
});
|
|
|
|
expect(definition.httpFetch).toBe('node-http1');
|
|
expect(serializeDefinition(definition).httpFetch).toBe('node-http1');
|
|
});
|
|
|
|
it('preserves refreshable bearer metadata in generated CLI definitions', () => {
|
|
const definition = normalizeDefinition({
|
|
name: 'stdio-refresh',
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
auth: 'refreshable_bearer',
|
|
refresh: {
|
|
token_endpoint: 'https://auth.example.com/token',
|
|
access_token_env: 'EXAMPLE_ACCESS_TOKEN',
|
|
},
|
|
});
|
|
|
|
expect(definition.auth).toBe('refreshable_bearer');
|
|
expect(definition.refresh).toEqual({
|
|
tokenEndpoint: 'https://auth.example.com/token',
|
|
accessTokenEnv: 'EXAMPLE_ACCESS_TOKEN',
|
|
});
|
|
expect(serializeDefinition(definition).refresh).toEqual(definition.refresh);
|
|
});
|
|
|
|
it('wraps single-token stdio commands when passed via --command', () => {
|
|
const args = ['--command', './scripts/mcp-server.ts'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.command).toBeDefined();
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec).toEqual({ command: './scripts/mcp-server.ts' });
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('mcp-server');
|
|
});
|
|
|
|
it('treats positional inline commands as generate-cli targets', () => {
|
|
const args = ['npx -y chrome-devtools-mcp@latest'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.command).toBeDefined();
|
|
expect(parsed.server).toBeUndefined();
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.command).toBe('npx');
|
|
expect(spec.args).toEqual(['-y', 'chrome-devtools-mcp@latest']);
|
|
});
|
|
|
|
it('keeps bare names positional when no whitespace is present', () => {
|
|
const args = ['linear'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.server).toBe('linear');
|
|
expect(parsed.command).toBeUndefined();
|
|
});
|
|
|
|
it('handles inline commands with extra interior whitespace', () => {
|
|
const args = [' bun run ./cli.ts --stdio '];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
const spec = parsed.command as { command: string; args?: string[] };
|
|
expect(spec.command).toBe('bun');
|
|
expect(spec.args).toEqual(['run', './cli.ts', '--stdio']);
|
|
});
|
|
|
|
it('treats positional HTTPS URLs as ad-hoc servers and infers names', () => {
|
|
const args = ['https://mcp.context7.com/mcp'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.command).toBe('https://mcp.context7.com/mcp');
|
|
expect(parsed.server).toBeUndefined();
|
|
const inferred = parsed.command !== undefined ? inferNameFromCommand(parsed.command) : undefined;
|
|
expect(inferred).toBe('context7');
|
|
});
|
|
|
|
it('builds regenerate commands honoring global flags and invocation overrides', () => {
|
|
const definition: SerializedServerDefinition = {
|
|
name: 'demo',
|
|
description: 'Demo server',
|
|
command: { kind: 'http', url: 'https://demo.mcp' },
|
|
};
|
|
const invocation = {
|
|
outputPath: 'out.ts',
|
|
runtime: 'bun' as const,
|
|
timeoutMs: 45_000,
|
|
minify: true,
|
|
bundle: 'out.bundle.js',
|
|
includeTools: ['alpha', 'beta'],
|
|
};
|
|
const command = buildGenerateCliCommand(invocation, definition, { '--config': '/tmp/mcporter.json' });
|
|
expect(command).toContain('--config /tmp/mcporter.json');
|
|
expect(command).toContain('--server demo');
|
|
expect(command).toContain('--bundle out.bundle.js');
|
|
expect(command).toContain('--timeout 45000');
|
|
expect(command).toContain('--minify');
|
|
expect(command).toContain("--include-tools 'alpha,beta'");
|
|
});
|
|
|
|
it('parses include/exclude tool lists', () => {
|
|
const args = ['--include-tools', 'a,b', '--include-tools', 'b,c', '--exclude-tools', 'x, y'];
|
|
const parsed = parseGenerateFlags([...args]);
|
|
expect(parsed.includeTools).toEqual(['a', 'b', 'c']);
|
|
expect(parsed.excludeTools).toEqual(['x', 'y']);
|
|
});
|
|
});
|