Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ JSON):
### What each tool does

- `bazaar_search` — searches a Bazaar node's discovery endpoint. Takes `url`
(base URL of the Bazaar API) and an optional `query`. Returns the catalog of
discoverable resources, or a structured error such as `ERR_NO_RESULTS` or
`ERR_SERVICE_UNREACHABLE`.
(base URL of the Bazaar API) and an optional `query`. Returns the collection of
discoverable resources and count (or `{ resources: [], count: 0 }` when no matching
resources exist, distinguishing empty matches from unreachable services), or a structured
error such as `ERR_SERVICE_UNREACHABLE` or `ERR_URL_BLOCKED`.
- `paid_call` — makes a paid API call to a Bazaar resource, handling the x402
402 payment loop automatically. Takes `url` (full URL of the resource) and an
optional `method` (default `GET`). Returns the resource body, or a structured
Expand Down
9 changes: 5 additions & 4 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,13 @@ export class BazaarMcpServer {
const hasResources = (d: unknown): d is { resources: unknown[] } =>
typeof d === 'object' && d !== null && 'resources' in d && Array.isArray((d as Record<string, unknown>).resources);

if ((Array.isArray(data) && data.length === 0) || (hasResources(data) && data.resources.length === 0)) {
return this.createErrorResponse('ERR_NO_RESULTS', 'No resources found');
}
const rawList = Array.isArray(data) ? data : (hasResources(data) ? data.resources : []);
const resultPayload = typeof data === 'object' && data !== null && !Array.isArray(data)
? { ...data, resources: rawList, count: rawList.length }
: { resources: rawList, count: rawList.length };

return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
content: [{ type: 'text', text: JSON.stringify(resultPayload, null, 2) }],
};
} catch (e: any) {
if (e instanceof SsrfError) {
Expand Down
32 changes: 32 additions & 0 deletions tests/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,37 @@ describe('BazaarMcpServer bazaar_search SSRF guard (#407)', () => {
expect(response.isError).toBeFalsy();
expect(JSON.parse(response.content[0].text).resources[0].name).toBe('x');
});

it('returns success with empty collection when bazaar_search finds no resources (#414)', async () => {
const emptyFetchImpl = async () =>
new Response(JSON.stringify({ resources: [] }), { status: 200, headers: { 'content-type': 'application/json' } });
const server = new BazaarMcpServer({
fetchWithPayment: vi.fn(),
bazaarFetch: { allowedHosts: [], ...publicLookup, fetchImpl: emptyFetchImpl },
});
const handler = captureHandler(server);
const response = await handler({ params: { name: 'bazaar_search', arguments: { url: 'https://ok.example', query: 'nonexistent' } } });
expect(response.isError).toBeFalsy();
const payload = JSON.parse(response.content[0].text);
expect(payload.resources).toEqual([]);
expect(payload.count).toBe(0);
});

it('returns success with empty collection when endpoint returns empty array (#414)', async () => {
const emptyArrayFetchImpl = async () =>
new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } });

const server = new BazaarMcpServer({
fetchWithPayment: vi.fn(),
bazaarFetch: { allowedHosts: [], ...publicLookup, fetchImpl: emptyArrayFetchImpl },
});
const handler = captureHandler(server);
const response = await handler({ params: { name: 'bazaar_search', arguments: { url: 'https://ok.example' } } });
expect(response.isError).toBeFalsy();
const payload = JSON.parse(response.content[0].text);
expect(payload.resources).toEqual([]);
expect(payload.count).toBe(0);
});
});

describe('BazaarMcpServer argument validation (#411)', () => {
Expand Down Expand Up @@ -483,3 +514,4 @@ describe('BazaarMcpServer bazaar_search upstream failures (#349)', () => {
expect(body.reason).toContain('not valid JSON');
});
});

Loading