Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion .changeset/research-preview-docs-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@upstash/context7-mcp": minor
---

Research preview: a `submit-docs-feedback` tool sends feedback on the documentation that `query-docs` returned (helpful or not, what was missing or wrong, and a short comment) from the agent or the user. It is listed only with research preview on: `--research-preview` or `CONTEXT7_RESEARCH_PREVIEW=1` for stdio, and `?research_preview=1` on the MCP URL for the hosted server.
Research preview: a `submit-docs-feedback` tool sends feedback on the documentation that `query-docs` returned: a score from 0 to 5, what was wrong, and a short comment. It is listed only with research preview on: `--research-preview` or `CONTEXT7_RESEARCH_PREVIEW=1` for stdio, and `?research_preview=1` on the MCP URL for the hosted server. With research preview on, the server instructions also ask the agent to send this feedback after it answers. API calls carry `X-Context7-Research-Preview: 1`, so Context7 can count how many people use the research preview.
17 changes: 12 additions & 5 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import {
} from "./lib/constants.js";
import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js";
import { QUERY_DOCS_TOOL, RESOLVE_LIBRARY_ID_TOOL } from "./lib/tool-names.js";
import { registerResearchPreviewTools } from "./lib/research-preview-tools.js";
import {
DOCS_FEEDBACK_INSTRUCTIONS,
registerResearchPreviewTools,
} from "./lib/research-preview-tools.js";
import { installProcessShutdown } from "./lib/process-shutdown.js";
import {
forceFlushTelemetry,
Expand Down Expand Up @@ -325,11 +328,15 @@ function createMcpServer(mcpContext: McpRequestContext, researchPreview: boolean
},
instructions: `Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, porting code between libraries, platforms, or SQL dialects, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer — your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, debugging, or reviewing code that does not depend on a library or platform API, or general programming concepts.`,
Do not use for: refactoring, debugging, or reviewing code that does not depend on a library or platform API, or general programming concepts.${researchPreview ? `\n\n${DOCS_FEEDBACK_INSTRUCTIONS}` : ""}`,
};
const server = mcpInstrumentation
? mcpInstrumentation.createServer(serverInfo, serverOptions, mcpContext)
: new McpServer(serverInfo, serverOptions);
const toolClientContext = (toolCtx: ServerContext): ClientContext => ({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper is not necessary. ClientContext is already built in exactly two places: the HTTP context object (const context: ClientContext = {...} in the request handler) and the stdio fallback in getClientContext.

Suggestion: set researchPreview there.

  • HTTP: read it from the same research_preview query parameter (isResearchPreviewValue).
  • stdio: use a module-level flag, like stdioSessionId.

Then this wrapper, the 3 changed call sites, the changed registerResearchPreviewTools signature and the ...(x ? { researchPreview: true } : {}) spread all go away.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fa3d031. The HTTP context reads research_preview from the query with isResearchPreviewValue, and stdio uses a module-level stdioResearchPreview next to stdioSessionId. The wrapper is gone, the call sites use getClientContext again, and registerResearchPreviewTools has its old signature.

...getClientContext(toolCtx),
...(researchPreview ? { researchPreview: true } : {}),
});

server.registerTool(
RESOLVE_LIBRARY_ID_TOOL,
Expand Down Expand Up @@ -391,7 +398,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
},
},
async ({ query, libraryName }: { query: string; libraryName: string }, toolCtx) => {
const ctx = getClientContext(toolCtx);
const ctx = toolClientContext(toolCtx);
const searchResponse = await searchLibraries(query, libraryName, ctx);

if (!searchResponse.results || searchResponse.results.length === 0) {
Expand Down Expand Up @@ -469,7 +476,7 @@ If you know the programming language the user is working in, pass it as \`langua
{ query, libraryId, language }: { query: string; libraryId: string; language?: string },
toolCtx
) => {
const ctx = getClientContext(toolCtx);
const ctx = toolClientContext(toolCtx);
const response = await fetchLibraryContext({ query, libraryId, language }, ctx);
maybeElicitAuthSignIn(server, ctx);
recordToolCallTelemetry(ctx, response.outcome);
Expand All @@ -489,7 +496,7 @@ If you know the programming language the user is working in, pass it as \`langua
}
);

if (researchPreview) registerResearchPreviewTools(server, getClientContext);
if (researchPreview) registerResearchPreviewTools(server, toolClientContext);

return server;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/mcp/src/lib/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export function generateHeaders(context: ClientContext): Record<string, string>
if (context.transport) {
headers["X-Context7-Transport"] = context.transport;
}
if (context.researchPreview) {
headers["X-Context7-Research-Preview"] = "1";
}

return headers;
}
29 changes: 16 additions & 13 deletions packages/mcp/src/lib/research-preview-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { recordToolCallTelemetry } from "./auth-lifecycle-telemetry.js";
import { SUBMIT_DOCS_FEEDBACK_TOOL } from "./tool-names.js";
import type { ClientContext, DocsFeedback } from "./types.js";

/** Added to the server instructions with research preview on, so agents remember the feedback tool. */
export const DOCS_FEEDBACK_INSTRUCTIONS = `After you answer a question with documentation from 'query-docs', call '${SUBMIT_DOCS_FEEDBACK_TOOL}' once for that library with a score from 0 to 5 and a short comment on the docs. The user turned on this research preview to collect this feedback.`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'query-docs' is a literal string here, but QUERY_DOCS_TOOL exists and the next tool name already uses its constant. Use ${QUERY_DOCS_TOOL}.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fa3d031. It uses ${QUERY_DOCS_TOOL} now.


/**
* Experimental tools, registered only with research preview on
* (`--research-preview`, `CONTEXT7_RESEARCH_PREVIEW=1` or `?research_preview=1`).
Expand All @@ -20,10 +23,10 @@ export function registerResearchPreviewTools(
title: "Submit Documentation Feedback",
description: `Sends feedback on documentation that 'Query Documentation' returned, so Context7 can fix its docs.

Call it once after you used the documentation of a library:
- helpful: true when the docs answered the question.
- helpful: false when they did not, with the issue: 'missing' (the docs do not cover it), 'outdated' (they describe an old version), 'incorrect' (the code or facts are wrong), 'irrelevant' (the results were about something else) or 'other'.
Also call it when the user comments on the documentation, with author 'user' and their words in comment.
Call it once after you used the documentation of a library to answer a question:
- score: how well the docs answered it, from 0 (useless) to 5 (fully answered with correct, current code).
- issue: what was wrong, only when the score is below 5: 'missing' (the docs do not cover it), 'outdated' (they describe an old version), 'incorrect' (the code or facts are wrong), 'irrelevant' (the results were about something else) or 'other'.
- comment: what was missing or wrong, or what was good, in one or two sentences. When the user comments on the documentation, pass on their words.

Do not include sensitive or confidential information such as API keys, passwords, credentials, personal data, or proprietary code.`,
inputSchema: z.object({
Expand All @@ -33,21 +36,21 @@ Do not include sensitive or confidential information such as API keys, passwords
"The Context7-compatible library ID the feedback is about, as passed to 'query-docs' (e.g., '/vercel/next.js')."
),
query: z.string().optional().describe("The 'query-docs' query the feedback is about."),
helpful: z.boolean().describe("Whether the documentation answered the question."),
score: z
.number()
.int()
.min(0)
.max(5)
.describe("How well the docs answered the question, from 0 (useless) to 5 (fully)."),
issue: z
.enum(["missing", "outdated", "incorrect", "irrelevant", "other"])
.optional()
.describe("What was wrong. Only when helpful is false."),
.describe("What was wrong. Only when the score is below 5."),
comment: z
.string()
.optional()
.describe(
"Short details: what was missing or wrong, or what the user said. Up to 1000 characters."
),
author: z
.enum(["agent", "user"])
.min(1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not match the app contract. The app trims the text, requires at least 1 character after the trim, and rejects more than 1,000 characters. Here " " passes and there is no maximum, so the agent gets a 400 from the app and not a schema error. Suggestion: .trim().min(1).max(1000).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fa3d031. The comment is .trim().min(1).max(1000) now, the same as the app.

.describe(
"'agent' for your own judgment of the docs, 'user' when you pass on the user's feedback."
"What was missing, wrong or good, in one or two sentences. Up to 1000 characters."
),
}),
annotations: {
Expand Down
7 changes: 4 additions & 3 deletions packages/mcp/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ export type ContextRequest = {
export type DocsFeedback = {
libraryId: string;
query?: string;
helpful: boolean;
score: number;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DocsFeedback copies the zod input schema by hand (the issue enum is in both places). Suggestion: move the schema to a constant and use export type DocsFeedback = z.infer<typeof docsFeedbackSchema>.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fa3d031. The schema is the docsFeedbackSchema constant, and DocsFeedback is z.infer<typeof docsFeedbackSchema>. I removed the copy from types.ts.

issue?: "missing" | "outdated" | "incorrect" | "irrelevant" | "other";
comment?: string;
author: "agent" | "user";
comment: string;
};

export type ContextResponse = {
Expand All @@ -57,6 +56,8 @@ export interface ClientContext {
mcpAuthMode?: "observe" | "required";
transport?: "stdio" | "http";
sessionId?: string;
/** Research preview is on; sent as `X-Context7-Research-Preview: 1` so the app counts preview use. */
researchPreview?: boolean;
/** Mutable: set by the upstream API layer when the backend signals the
* client should be prompted to sign in. Read by the auth-prompt wrapper. */
shouldPrompt?: boolean;
Expand Down
5 changes: 3 additions & 2 deletions packages/mcp/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,10 +627,9 @@ describe("research preview", () => {
const feedback = {
libraryId: "/vercel/next.js",
query: "app router caching",
helpful: false,
score: 2,
issue: "outdated",
comment: "Shows the Next.js 14 cache defaults.",
author: "agent",
};
const result = await client.callTool({ name: "submit-docs-feedback", arguments: feedback });

Expand All @@ -639,6 +638,7 @@ describe("research preview", () => {
const apiCall = requests.find((request) => request.path === "/v2/feedback");
expect(apiCall?.headers.authorization).toBe("Bearer ctx7sk-local-test");
expect(apiCall?.headers["x-context7-source"]).toBe("mcp-server");
expect(apiCall?.headers["x-context7-research-preview"]).toBe("1");
} finally {
await client.close();
}
Expand Down Expand Up @@ -878,6 +878,7 @@ describe.each([
expect(apiCalls[0].query.get("libraryId")).toBe("/vercel/next.js");
expect(apiCalls[0].query.get("query")).toBe("app router");
expect(apiCalls[0].headers["x-context7-transport"]).toBe(transportKind);
expect(apiCalls[0].headers["x-context7-research-preview"]).toBeUndefined();
if (transportKind === "http") {
expect(apiCalls[0].headers["mcp-client-ip-assertion"]).toMatch(/^v1:/);
expect(
Expand Down
Loading