Skip to content

Commit fc0eba1

Browse files
committed
fix: scope catalog reads to request context
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
1 parent ee4c07c commit fc0eba1

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

src/mcp/handlers-official-tools.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,13 @@ export async function handleListCatalog(args: unknown, context?: InstanceContext
170170
return { success: false, code: 'INVALID_ARGS', error: parsed.error.issues.map(i => `${i.path.join('.') || 'input'}: ${i.message}`).join('; ') };
171171
}
172172
const { kind, query, limit } = parsed.data;
173-
const api = getN8nApiClient(context);
174-
if (!api) return { success: false, code: 'NOT_CONFIGURED', error: 'n8n API not configured. Set N8N_API_URL and N8N_API_KEY.' };
175173

176174
if (kind === 'tags') {
175+
if (!publicApiMatchesContext(context)) {
176+
return { success: false, kind, code: 'NOT_CONFIGURED', error: PUBLIC_API_CONTEXT_HINT } as McpToolResponse;
177+
}
178+
const api = getN8nApiClient(context);
179+
if (!api) return { success: false, kind, code: 'NOT_CONFIGURED', error: 'n8n API not configured. Set N8N_API_URL and N8N_API_KEY.' } as McpToolResponse;
177180
try {
178181
const tags = (await api.listTags({ limit: 250 })).data.map(t => ({ id: String(t.id), name: t.name }));
179182
return { success: true, kind, backend: 'public-api', data: { items: filterItems(tags, query, limit) } } as McpToolResponse;

tests/unit/mcp/handlers-official-tools.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ vi.mock('@/mcp/official-mcp-access', async (orig) => ({ ...(await orig<any>()),
55
const api = vi.hoisted(() => ({ getN8nApiClient: vi.fn() }));
66
vi.mock('@/mcp/handlers-n8n-manager', () => ({ getN8nApiClient: api.getN8nApiClient }));
77
import { handleExploreNodeResources, handleListCatalog, callOfficialTool, resolveProjectChoices } from '@/mcp/handlers-official-tools';
8+
import { PUBLIC_API_CONTEXT_HINT } from '@/services/mcp-exposure';
89
import { N8nApiError } from '@/utils/n8n-errors';
910
import { N8nOfficialMcpClient } from '@/services/n8n-official-mcp-client';
1011
import { startFakeOfficialMcp, FakeOfficialMcp, FakeTool } from '../../helpers/fake-official-mcp-server';
@@ -174,6 +175,28 @@ describe('handleListCatalog', () => {
174175
const r = await handleListCatalog({ kind: 'tags', query: 'prod', limit: 1 });
175176
expect(r).toMatchObject({ success: true, kind: 'tags', backend: 'public-api', data: { items: [{ id: 't1', name: 'Prod' }] } });
176177
});
178+
it('does not fall back to the environment API for tags on a url + token context', async () => {
179+
api.getN8nApiClient.mockReturnValue({ listTags: vi.fn() });
180+
181+
const r = await handleListCatalog(
182+
{ kind: 'tags' },
183+
{ n8nApiUrl: 'https://other.test.com', n8nMcpAccessToken: 'tok' }
184+
);
185+
186+
expect(api.getN8nApiClient).not.toHaveBeenCalled();
187+
expect(r).toMatchObject({ success: false, code: 'NOT_CONFIGURED', error: PUBLIC_API_CONTEXT_HINT });
188+
});
189+
it('uses the context-specific hint when projects have no matching client', async () => {
190+
api.getN8nApiClient.mockReturnValue(null);
191+
access.getOfficialMcpClient.mockReturnValue(null);
192+
193+
const r = await handleListCatalog(
194+
{ kind: 'projects' },
195+
{ n8nApiUrl: 'https://other.test.com', n8nMcpAccessToken: 'tok' }
196+
);
197+
198+
expect(r).toMatchObject({ success: false, code: 'NOT_CONFIGURED', error: PUBLIC_API_CONTEXT_HINT });
199+
});
177200
it('rejects unknown kinds', async () => {
178201
expect(await handleListCatalog({ kind: 'users' })).toMatchObject({ success: false, code: 'INVALID_ARGS' });
179202
});

0 commit comments

Comments
 (0)