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
4 changes: 4 additions & 0 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ Do not call this tool more than 3 times per question.`,
text: response.data,
},
],
// Flag failures at the protocol level. Without this an unset `isError`
// defaults to success, so a caller that branches on `isError` treats an
// error message (invalid ID, library not found) as documentation.
...(response.isError ? { isError: true } : {}),
};
}
);
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export async function fetchLibraryContext(
if (!response.ok) {
const errorMessage = await parseErrorResponse(response, context.apiKey);
console.error(errorMessage);
return { data: errorMessage };
return { data: errorMessage, isError: true };
}

const text = await response.text();
Expand All @@ -170,6 +170,6 @@ export async function fetchLibraryContext(
} catch (error) {
const errorMessage = `Error fetching library context. Please try again later. ${error}`;
console.error(errorMessage);
return { data: errorMessage };
return { data: errorMessage, isError: true };
}
}
6 changes: 6 additions & 0 deletions packages/mcp/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export type ContextRequest = {

export type ContextResponse = {
data: string;
/**
* True when `data` is an error message rather than documentation, so the
* caller can surface it as a tool error (`isError: true`) instead of letting
* it default to a successful result.
*/
isError?: boolean;
};

export interface ClientContext {
Expand Down
58 changes: 58 additions & 0 deletions packages/mcp/test/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { fetchLibraryContext } from "../src/lib/api.js";

// fetchLibraryContext calls global fetch; stub it per case.
afterEach(() => {
vi.unstubAllGlobals();
});

function stubFetch(response: Partial<Response> & { ok: boolean }) {
// fetchLibraryContext reads response.headers (auth-prompt signal), so every
// stub needs a real Headers object.
const full = { headers: new Headers(), ...response } as Response;
vi.stubGlobal(
"fetch",
vi.fn(async () => full)
);
}

describe("fetchLibraryContext", () => {
test("flags a non-ok API response as an error", async () => {
// A failed lookup (e.g. 404 for a nonexistent library) must be marked so
// the tool result can set isError:true instead of defaulting to success.
stubFetch({
ok: false,
status: 404,
json: async () => ({ message: "Library not found." }),
});

const result = await fetchLibraryContext({ query: "q", libraryId: "/no/such-lib" });
expect(result.isError).toBe(true);
expect(result.data).toBe("Library not found.");
});

test("flags a network failure as an error", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw new Error("network down");
})
);

const result = await fetchLibraryContext({ query: "q", libraryId: "/vercel/next.js" });
expect(result.isError).toBe(true);
expect(result.data).toContain("Error fetching library context");
});

test("does not flag a successful documentation response", async () => {
stubFetch({
ok: true,
status: 200,
text: async () => "# Some real documentation",
});

const result = await fetchLibraryContext({ query: "q", libraryId: "/vercel/next.js" });
expect(result.isError).toBeUndefined();
expect(result.data).toBe("# Some real documentation");
});
});