Two mcplocals sharing the same config template (`vllm-local-qwen3`)
no longer collide on mcpd's cluster-wide unique-name constraint.
Each publisher can append a suffix derived from hostname (or any
other stable per-host identifier) so the wire-side names become
distinct (`vllm-local-qwen3-alice`, `vllm-local-qwen3-bob`).
Pair with an explicit `poolName` (v4) and the rows still appear as
one logical pool — agents pinned to any member load-balance across
both.
Config (`~/.mcpctl/config.json`):
{
"publisher": { "suffix": "auto" } // → os.hostname() sanitized
// or { "suffix": "alice" } for explicit override
}
Or via env: `MCPCTL_PUBLISHER_SUFFIX=alice` (operations override).
Resolution order: env var → config.publisher.suffix → empty
(legacy behavior, no mangling). Sanitization lowercases, replaces
non-`[a-z0-9-]` runs with `-`, strips leading/trailing dashes —
the result must satisfy mcpd's name validation, otherwise the
register POST would 422.
Wire shape: RegistrarPublishedProvider gets an optional
`publishName` field. When set, the wire payload's `name` is
`publishName` (suffixed); when not, today's `provider.name`.
Inbound infer/wake task lookups match `publishName ?? provider.name`
so the local registry stays addressable by its original name —
SSE frames carrying the suffixed wire name still find their
provider.
Agents are forwarded with their own suffixed name AND a
`llmName` rewritten through the same per-local→wire map so the
agent rows pin to the suffixed Llm wire name (otherwise
registerVirtualAgents would 404).
Tests: 8 new tests covering applyPublisherSuffix (empty, normal,
length limit, exact-100) and loadPublisherSuffix (env override,
absent, sanitization, dash stripping). Existing registrar tests
untouched — no suffix means no behavior change.
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
/**
|
|
* v6 unit tests for the per-publisher namespacing helpers
|
|
* (loadPublisherSuffix + applyPublisherSuffix). The wiring through
|
|
* mcplocal/main.ts is exercised at the smoke level; these tests
|
|
* cover the pure logic so name-mangling regressions are caught fast.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { resetConfigCache, applyPublisherSuffix, loadPublisherSuffix } from '../src/http/config.js';
|
|
|
|
describe('applyPublisherSuffix', () => {
|
|
it('passes the name through unchanged when the suffix is empty', () => {
|
|
// Empty suffix is the legacy code path — pre-v6 behavior.
|
|
expect(applyPublisherSuffix('vllm-local-qwen3', '')).toBe('vllm-local-qwen3');
|
|
});
|
|
|
|
it('appends -<suffix> to a normal name', () => {
|
|
expect(applyPublisherSuffix('vllm-local-qwen3', 'alice')).toBe('vllm-local-qwen3-alice');
|
|
});
|
|
|
|
it('throws when the combined length exceeds the 100-char limit', () => {
|
|
// Name validation on mcpd's side caps at 100; we'd rather error
|
|
// loud at enqueue time than have the register POST 422 later.
|
|
const longName = 'a'.repeat(95);
|
|
expect(() => applyPublisherSuffix(longName, 'alicebob')).toThrow(/100-char limit/);
|
|
});
|
|
|
|
it('lets a name + short suffix exactly hit 100', () => {
|
|
const name = 'a'.repeat(95);
|
|
expect(applyPublisherSuffix(name, 'four')).toHaveLength(100);
|
|
});
|
|
});
|
|
|
|
describe('loadPublisherSuffix', () => {
|
|
beforeEach(() => {
|
|
resetConfigCache();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
resetConfigCache();
|
|
});
|
|
|
|
it('honors MCPCTL_PUBLISHER_SUFFIX env var (operations override)', () => {
|
|
// Env override takes precedence so an operator can flip the suffix
|
|
// for a one-off run without touching ~/.mcpctl/config.json.
|
|
expect(loadPublisherSuffix({ MCPCTL_PUBLISHER_SUFFIX: 'BoB.Lap-top' })).toBe('bob-lap-top');
|
|
});
|
|
|
|
it('returns empty string when neither env nor config sets a suffix', () => {
|
|
expect(loadPublisherSuffix({})).toBe('');
|
|
});
|
|
|
|
it('sanitizes uppercase + special chars + collapses runs', () => {
|
|
expect(loadPublisherSuffix({ MCPCTL_PUBLISHER_SUFFIX: "Alice's-MacBook.Pro!!" })).toBe('alice-s-macbook-pro');
|
|
});
|
|
|
|
it('strips leading/trailing dashes after sanitization', () => {
|
|
expect(loadPublisherSuffix({ MCPCTL_PUBLISHER_SUFFIX: '___bob___' })).toBe('bob');
|
|
});
|
|
});
|