ungated-global: system project whose servers ride every project, pre-gate
Some checks failed
CI/CD / lint (pull_request) Successful in 1m12s
CI/CD / test (pull_request) Successful in 1m26s
CI/CD / typecheck (pull_request) Successful in 3m22s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 5m15s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m12s
CI/CD / test (pull_request) Successful in 1m26s
CI/CD / typecheck (pull_request) Successful in 3m22s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 5m15s
CI/CD / publish (pull_request) Has been skipped
New seed-once system project `ungated-global`: every server attached to it is merged into every project's MCP endpoint in front of the gate (listed and callable before begin_session), and its project prompt is prepended to every project's instructions. - mcpd: bootstrapUngatedGlobal seeds the project once-ever (marker key `ungatedGlobalSeeded` in mcpctl-system-settings), with default server `calcmcp` (patched CalcMCP fork, mysources.co.uk/michal/calcmcp:1.1.0) and a "never do math yourself, always use calcmcp" prompt. Deletions are durable: `mcpctl delete server calcmcp -p ungated-global` stays deleted. - mcpd: GET /projects/:id/servers appends global servers flagged `ungatedGlobal: true` (direct attachment wins, no self-merge); GET /projects/:id/instructions prepends the global prompt and lists the global servers. - mcplocal: discovery surfaces the flagged names; the gate plugin exempts them — gated tools/list returns begin_session + exempt tools, calls to them pass through without ungating, and initialize instructions list them under "Available immediately (no begin_session required)". - templates/calcmcp.yaml so a deleted default can be re-created by hand. - docs/ungated-global.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,8 @@ function setupPluginRouter(opts: {
|
||||
/** When set, the heavy provider's complete() rejects — exercises the loud fallback. */
|
||||
llmError?: boolean;
|
||||
byteBudget?: number;
|
||||
/** Gate-exempt server names (ungated-global project). */
|
||||
ungatedServers?: string[];
|
||||
} = {}): { router: McpRouter; mcpdClient: McpdClient } {
|
||||
const router = new McpRouter();
|
||||
const prompts = opts.prompts ?? samplePrompts;
|
||||
@@ -105,6 +107,7 @@ function setupPluginRouter(opts: {
|
||||
gated: opts.gated !== false,
|
||||
providerRegistry,
|
||||
byteBudget: opts.byteBudget,
|
||||
...(opts.ungatedServers ? { ungatedServers: opts.ungatedServers } : {}),
|
||||
});
|
||||
router.setPlugin(gatePlugin);
|
||||
|
||||
@@ -160,6 +163,63 @@ describe('Gate Plugin via setPlugin()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('gate-exempt (ungated-global) servers', () => {
|
||||
it('lists exempt server tools alongside begin_session while gated', async () => {
|
||||
const { router } = setupPluginRouter({ ungatedServers: ['calcmcp'] });
|
||||
router.addUpstream(mockUpstream('calcmcp', { tools: [{ name: 'symbolic_tool' }] }));
|
||||
router.addUpstream(mockUpstream('ha', { tools: [{ name: 'get_entities' }] }));
|
||||
await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
|
||||
|
||||
const res = await router.route(
|
||||
{ jsonrpc: '2.0', id: 2, method: 'tools/list' },
|
||||
{ sessionId: 's1' },
|
||||
);
|
||||
const names = (res.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name);
|
||||
expect(names).toContain('begin_session');
|
||||
expect(names).toContain('calcmcp/symbolic_tool');
|
||||
expect(names).not.toContain('ha/get_entities');
|
||||
});
|
||||
|
||||
it('calls an exempt tool while gated without intercept and stays gated', async () => {
|
||||
const { router } = setupPluginRouter({ ungatedServers: ['calcmcp'] });
|
||||
router.addUpstream(mockUpstream('calcmcp', { tools: [{ name: 'symbolic_tool' }] }));
|
||||
await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
|
||||
|
||||
const callRes = await router.route(
|
||||
{ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'calcmcp/symbolic_tool', arguments: { operation: 'simplify', expression: '1+1' } } },
|
||||
{ sessionId: 's1' },
|
||||
);
|
||||
const content = (callRes.result as { content: Array<{ text: string }> }).content;
|
||||
// Real upstream result, not a gate intercept briefing (the router
|
||||
// strips the server prefix when forwarding to the upstream)
|
||||
expect(content[0]!.text).toBe('Called symbolic_tool');
|
||||
|
||||
// Session must STILL be gated: non-exempt catalog stays hidden
|
||||
const toolsRes = await router.route(
|
||||
{ jsonrpc: '2.0', id: 3, method: 'tools/list' },
|
||||
{ sessionId: 's1' },
|
||||
);
|
||||
const names = (toolsRes.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name);
|
||||
expect(names).toContain('begin_session');
|
||||
});
|
||||
|
||||
it('lists exempt tools under "Available immediately" in initialize instructions', async () => {
|
||||
const { router } = setupPluginRouter({ ungatedServers: ['calcmcp'] });
|
||||
router.addUpstream(mockUpstream('calcmcp', { tools: [{ name: 'symbolic_tool' }] }));
|
||||
router.addUpstream(mockUpstream('ha', { tools: [{ name: 'get_entities' }] }));
|
||||
|
||||
const res = await router.route(
|
||||
{ jsonrpc: '2.0', id: 1, method: 'initialize' },
|
||||
{ sessionId: 's1' },
|
||||
);
|
||||
const instructions = (res.result as { instructions?: string }).instructions ?? '';
|
||||
expect(instructions).toContain('Available immediately (no begin_session required):');
|
||||
expect(instructions).toContain('calcmcp_symbolic_tool');
|
||||
expect(instructions).toContain('Available MCP server tools (accessible after begin_session):');
|
||||
expect(instructions).toContain('ha_get_entities');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tools/list gating', () => {
|
||||
it('shows only begin_session when session is gated', async () => {
|
||||
const { router } = setupPluginRouter();
|
||||
|
||||
Reference in New Issue
Block a user