MCP Server
CVD Portal exposes a Model Context Protocol (MCP) server so AI agents can act on the platform directly, the same way a script would use the REST API. An agent connected to it can submit a vulnerability report, read a company's CRA compliance status, and generate a CSAF advisory, without any custom integration code.
There are two servers, one for each side of a disclosure.
| Server | Endpoint | Who it is for | Auth |
|---|---|---|---|
| Manufacturer | /api/mcp | The company operating a portal, acting inside its own workspace | OAuth 2.1 token or API key |
| Researcher | /api/mcp/public | Anyone reporting a vulnerability to a manufacturer | None |
The rest of this page covers the manufacturer server. The researcher server is documented under Public MCP server below.
The manufacturer server accepts companies on the Reporting plan or higher. Each tool also checks the plan.
- Reporting.
get_getting_started,get_next_deadline,get_next_task, the Article 14 toolsrecord_article14_decision,prepare_article14_stageandopen_incident,record_remediation_decision,upload_sbom,list_productswith the id and name only, andsubmit_jobandget_job_statusfor drafting jobs. - Compliance. All other tools. These are the CRA product tools,
get_conformity_gaps,get_clause_mapping,record_assessment_result, classification and conformity jobs, submission triage, advisories,get_compliance_statusandask_expert.
On the Reporting plan, an API key can carry only the read scope, so it reaches only the read tools. Use browser sign-in (OAuth) for the Article 14 and remediation tools. On the Compliance plan, an API key can carry any MCP scope. The REST API under /api/v1 still needs the Enterprise plan, also for a key issued on Compliance. No MCP tool files a notification with ENISA. You file on the Single Reporting Platform yourself.
The server accepts either an OAuth 2.1 access token obtained via authorization code grant with PKCE, or a legacy Bearer API key. When accessed without credentials, it returns an HTTP 401 status with an RFC 9728 WWW-Authenticate header pointing to /.well-known/oauth-protected-resource so compatible agents can discover the authorization server and register automatically.
Endpoint
https://cvdportal.com/api/mcp
The transport is streamable HTTP. There are two ways to authenticate.
| Method | Use it for | What happens |
|---|---|---|
| Browser sign-in (OAuth 2.1) | Claude Code, the Claude apps, and other MCP clients that a person uses | The client opens your browser. You sign in to CVD Portal and approve the access. The client stores and renews the token. |
| API key | Scripts, CI/CD pipelines, and servers with no browser | You send the key in the Authorization header on every request. |
Use browser sign-in when a person runs the agent. The token is bound to you, so the audit log names you, and it is the only credential that can act for a client company (see Acting for a client company). An API key belongs to the company, not to a person.
Connecting with browser sign-in
Claude Code
Add the server without a header:
claude mcp add --transport http cvd-portal https://cvdportal.com/api/mcp
Then, in an interactive claude session, run /mcp, select cvd-portal, and select Authenticate. Your browser opens.
Claude desktop app and claude.ai
Add a custom connector with the URL https://cvdportal.com/api/mcp and connect it. Your browser opens.
What you see in the browser
- The CVD Portal sign-in page, if you are not signed in already. Sign in with your password or with a sign-in provider such as Google, Microsoft or GitHub. If your account at that provider uses a passkey, you confirm with your fingerprint or face there.
- The consent screen. It names the client, your company, and the permissions (scopes) the client asks for. The client chose its own name, so the screen marks it Unverified name. The box Authorization is sent to shows the host that receives the authorization. Check that host before you select Authorize Access.
- The browser returns to the client, and the client is connected.
How it works
The client needs no configuration beyond the URL. A request without a token gets HTTP 401 with a WWW-Authenticate header that points to /.well-known/oauth-protected-resource. From there the client finds the authorization server, registers itself (RFC 7591), and starts the authorization code flow with PKCE. An access token lasts 1 hour. The client renews it with a refresh token, which lasts 30 days. After 30 days without use, you sign in again.
The consent screen asks only for the MCP scopes: read, submissions:write, submissions:triage, submissions:communicate, products:write, sbom:write, article14:prepare, advisory:draft and conformity:prepare. MCP clients name the MCP server as the resource of the sign-in. For such a sign-in, the platform removes any REST-only scope such as gdpr:export, even when the client asks for it.
A client gets only the scopes that it registered. The platform removes each requested scope that the client did not register, and the consent screen does not show it. A client that asks for no scopes gets the MCP scopes that it registered. A client that asks only for scopes that it did not register gets the error invalid_scope and no consent screen.
The token can only call tools whose scope you approved on the consent screen. If every tool call fails with "OAuth token lacks the required scope", remove the connection, add it again, and check the permissions on the consent screen.
See Authentication for the endpoints and the standards they follow.
Connecting with an API key
Create and scope keys under Settings → API Keys in the dashboard. On the Reporting plan, you can create a key with only the read scope for the MCP server. On the Compliance plan, you can create a key with any combination of the MCP scopes: read, submissions:write, submissions:triage, submissions:communicate, products:write, sbom:write, article14:prepare, advisory:draft and conformity:prepare. Keys with other scopes need the Enterprise plan. A key with no scopes has full access. A scoped key can only call tools whose scope it carries.
Claude Code
claude mcp add --transport http cvd-portal https://cvdportal.com/api/mcp \
--header "Authorization: Bearer <your-api-key>"
Any MCP client
Point the client at https://cvdportal.com/api/mcp with the Authorization: Bearer <your-api-key> header. Clients that support the Model Context Protocol streamable HTTP transport discover the tools automatically.
Rolling out to your organisation
Use this section when you add CVD Portal for all people in a Claude Team or Claude Enterprise organisation. Each person signs in to CVD Portal with their own account. CVD Portal does not support central sign-in for the whole organisation.
Add the connector once
- An Owner or Primary Owner of the Claude organisation opens Organization settings, then Connectors. On Claude Enterprise, a person with a custom role that includes Manage access to Libraries can also do this.
- Select Add, then Custom, then Web. Enter a name and the URL
https://cvdportal.com/api/mcp. - Leave the OAuth Client ID and Client Secret empty. Claude registers itself with CVD Portal.
- Tell each person to find the connector, select Connect, and sign in to CVD Portal.
Invite each person under Settings → Team before they connect. A person who signs up without an invitation gets a new, empty company, and the connector then works in that company, not in yours. Each person approves the scopes on the consent screen. At each tool call, CVD Portal checks the scope and the role of that person. A MEMBER cannot call the tools that need ADMIN. The audit log names each person.
Do not put an API key in an organisation connector
An API key belongs to the company, not to a person. Each person who uses a shared key gets the full access of that key, and the role of the person is not checked. The audit log does not name the person. An API key cannot act for a client company. Use API keys only for scripts and pipelines.
Remove access for one person
Remove the person under Settings → Team. This revokes all OAuth tokens of the person immediately. A password change or a password reset also revokes them.
If you disable the person only in your identity provider, the connector continues to work. CVD Portal has no SCIM, and a refresh token stays valid for up to 30 days. Always remove the person in CVD Portal too.
What CVD Portal does not support
- A list of connections. No page shows which people have connected which clients.
- Revocation of one client. You cannot revoke one client, for example Claude, and keep the other connections of the same person. Removing the person revokes all of their tokens.
- An allowlist of clients. Any MCP client can register itself. The consent screen marks the client name as Unverified name, and each person decides whether to approve it.
- A pre-registered client. You cannot create an OAuth client ID and secret for your organisation in the dashboard.
Scopes
Scoped API keys and OAuth tokens restrict which tools an agent can call.
| Scope | Description |
|---|---|
read | Read vulnerabilities, products, compliance and SBOM metadata |
submissions:write | Submit vulnerability reports |
submissions:triage | Triage and progress vulnerability reports |
submissions:communicate | Draft messages to reporters for approval |
article14:prepare | Prepare CRA Article 14 notification dossiers |
advisory:draft | Draft CSAF and OpenVEX security advisories |
products:write | Register products, record classifications, and build the threat model |
conformity:prepare | Run conformity jobs, record assessments, and sign off Annex I requirements |
sbom:write | Upload an SBOM with upload_sbom. On the REST API this scope also needs the Enterprise plan |
Tools
| Tool | Scope required | What it does |
|---|---|---|
list_submissions | read | Lists vulnerability reports with optional stage or status filtering. |
get_submission | read | Returns decrypted case details excluding reporter contact information. |
get_submission_journey | read | Returns the active journey stages, current status, and next step. |
acknowledge_submission | submissions:triage | Acknowledges a report and sends the standard template message. |
triage_submission | submissions:triage | Sets severity, CVSS vector and score, and affected components. |
link_submission_product | submissions:triage | Links a vulnerability report to a workspace product. |
record_article14_decision | article14:prepare | Records whether a vulnerability is actively exploited under CRA Article 14. |
prepare_article14_stage | article14:prepare | Saves draft narrative fields and generates copy-ready SRP text. |
assign_coordinator | submissions:triage | Assigns a vulnerability coordinator from your team. |
record_remediation_decision | submissions:triage | Records the remediation decision and target deadline. |
record_fix_verification | submissions:triage | Confirms that a deployed fix has been verified in production. |
close_submission | submissions:triage | Closes a vulnerability report as RESOLVED or DISMISSED with justification. |
draft_reporter_message | submissions:communicate | Enqueues a message to the vulnerability reporter for human approval. |
request_advisory_publication | advisory:draft | Enqueues publication of an advisory draft for human approval. |
open_incident | submissions:write | Creates a severe incident report and starts CRA Article 14 tracking. Severity is HIGH or CRITICAL, and HIGH when omitted. |
submit_vulnerability | submissions:write | Files a disclosure report in your inbox, or in a client inbox with onBehalfOfCompanyId. Requires a description. The response names the company that received the report. The audit log of that company records the report with the OAuth client or the API key. |
get_compliance_status | read | Returns the verification status of your company account, ACCOUNT_VERIFIED or ACCOUNT_NOT_VERIFIED, with recommendations. It checks two account flags and reads no product, so it is not a verdict on a product. For a product, read readyForDocAssembly from get_conformity_gaps. |
generate_csaf_advisory | read | Returns the machine-readable advisory document for an existing advisory. |
assess_response | read | Evaluates an incident or vulnerability response plan under CRA Article 14. |
get_getting_started | read | Returns the ordered first steps for the company, and the portal slug, the published portal URL and whether the portal is on. Call it first. |
get_next_deadline | read | Returns the soonest open Article 14 deadline. |
get_next_task | read | Returns the next open CRA obligation. |
submit_job | depends on the job type | Runs a long job in the background. See Background jobs. |
get_job_status | read | Returns the status, progress and result of a job. The plan check follows the job type, the same as submit_job. |
ask_expert | read | Answers a CRA question with citations. answer is plain text. When the model returns more fields beside its answer, they are in details. Advisory only. A rule check reads each answer for article numbers of the 2022 CRA proposal and for wrong statements of Article 13(8) and Article 14. When it finds one, citationCheck lists each finding with its sentence, and a note is added to the end of answer. |
lookup_regulatory_reference | read | Searches the regulatory reference library by topic, document code, clause number or CRA product category. With standard, only the documents of that standard are searched. The tool ignores case, spaces and hyphens in the code, so FprEN 40000-1-2 and fpren-40000-1-2 find the same documents. A standard that matches no document returns count: 0 and a message with the known codes of the same publisher. |
link_submission_product, record_article14_decision, prepare_article14_stage, assign_coordinator and close_submission need the ADMIN role, as in the dashboard. With a browser sign-in token, the platform reads your role in the token's company at each call. That is your account role in your own company, or your membership role in a client company where you hold a seat. A MEMBER gets an error even when the token has the scope. A plain API key belongs to the company, so its scopes alone decide.
CRA product tools
These tools cover the product journey, from registration to the technical file. They need the Compliance plan features of the company they act for.
| Tool | Scope required | What it does |
|---|---|---|
list_products | read | Lists active products with their ids, the decided classification, the unconfirmed suggestion, and progress counts. On the Reporting plan it returns only the id and the name, and detail is basic. |
create_product | products:write | Registers a product. Records a class suggestion only. Refuses a duplicate name and returns the existing id. |
set_product_classification | products:write | Records the classification decision (Annex III or IV category, so the Article 32 route) and its legal basis. Send categoryId: null for a default-class product. An unknown categoryId is refused, and the error lists each valid id with its label, one per line. The response gives classificationDecidedAt, the justification, the standards applied and the Article 32 route in conformityRoute. Needs an OAuth token. |
set_notified_body | conformity:prepare or products:write | Records the notified body name and 4-digit identification number for a route that needs third-party assessment. This closes the notified_body_missing gap. The certificate number is optional. Returns required: false when the route allows Module A. Needs an OAuth token. |
generate_product_threats | products:write | Generates STRIDE threat candidates. Does not save. Check source: catalogue means generic entries, not an analysis. Each threat proposes initialMisconfiguration (true, false or null). Can take more than a minute. |
propose_threat_treatment | products:write | Proposes a mitigation and the residual risk for one threat. Does not save. Needs description (1 to 2000 characters), likelihood and impact. Both accept the five-level treatment scale and the scale that generate_product_threats returns, so a generated threat passes in as it is. Likelihood rare, unlikely, possible, likely or almost_certain, or low, medium, high, which map to unlikely, possible, almost_certain. Impact negligible, minor, moderate, major or catastrophic, or low, medium, high, critical, which map to minor, moderate, catastrophic, catastrophic. Each three-level word takes the highest level in its range. The response's inherent gives the mapped values. Write the same values on the risk row. |
get_clause_mapping | read | Returns the mapping in force for one CRA clause. clauseRef.part is 1 for Part I or 2 for Part II, and clauseRef.point is a number with an optional letter, for example {instrument:'CRA', annex:'I', part:'1', point:'2b'} for Annex I Part I (2)(b). The tool reads II as 2 and (2)(b) as 2b. clauseRef can also be a string: Annex I, Part I, (2)(b), the ref that get_threat_model returns, or the key CRA:I:1:2b. |
get_threat_model | read | Returns the saved assets, threats and risks, and every Annex I requirement with its key, evidence and sign-off. A requirement whose sign-off was revoked has lastRevokedBy and lastRevokedAt. Also returns the evidence attached to the product (attachedEvidence), and for each requirement the attached evidence that can support it (suggestedControlIds). annexIApplicability lists the 13 Part I(2) letters with the recorded decision. article13Duties lists the Article 13 paragraphs that take a record, with the stored status, evidence, owner and allowedStatuses. |
save_threat_model | products:write | Saves assets, threats and risks row by row. null deletes a row. Sets risk treatment. Any validation failure refuses the whole save. A row that has no value for a required field, or a value outside its type, is saved, and warnings names it, for example assets.a1: missing category, classification. An asset needs name, category and classification. A threat needs category, asset, description and status. A risk needs description. A row with no productId gets the id of the product, because the gap analysis reads only the rows of the product. A row counts in the export gate only when it is confirmed: an asset with confirmed: true, and a threat with source set to manual (or catalog for a catalogue threat). A threat that the agent confirms is recorded as confirmed by the person who owns the token, at the time of the save, with confirmedVia: "mcp". The server writes confirmedAt, confirmedBy and confirmedVia, and ignores the values that the agent sends. A token that does not belong to a person cannot confirm a threat, so the row is stored as a proposal (source: "ai") and warnings names it. A source other than manual, catalog or ai is also stored as a proposal. A catalogue baseline threat stays a baseline row until its description is rewritten for the product. A threat's initialMisconfiguration is stored as a proposal with initialMisconfigurationProposed: true. A cause that a person recorded stays, whatever the agent sends. |
set_annex_i_evidence | conformity:prepare or products:write | Records the evidence for Annex I requirements: a free-text note, links to evidence attached to the product, or both. This closes the no_requirement_evidenced gap. A link must name evidence that the product holds. It does not sign off. The row is marked as written by an agent, and reviewRequired lists these keys. The technical file status lists each one as "Review the agent evidence" until a named person signs the requirement off again, in the dashboard or with signoff_annex_i_requirements. A person's edit of the evidence does not close it. |
set_annex_i_applicability | conformity:prepare or products:write | Records whether each Annex I Part I(2) requirement, letters a to m, applies. not_applicable needs a justification. See Applicability and Article 13 duties over MCP. Needs an OAuth token. |
set_article13_duties | conformity:prepare or products:write | Records the status, evidence reference and owner of the Article 13 duties that CVD Portal does not record elsewhere. See Applicability and Article 13 duties over MCP. Needs an OAuth token. |
set_product_description_facts | conformity:prepare or products:write | Records the software versions that affect compliance and whether the product has hardware, Annex VII point 1(b) and 1(c). See Annex VII facts over MCP. Needs an OAuth token. |
set_support_period | conformity:prepare or products:write | Records the Article 13(8) support period, the expected time in use and how the period was determined, Annex VII point 4. See Annex VII facts over MCP. Needs an OAuth token. |
signoff_annex_i_requirements | conformity:prepare or products:write | Signs off or revokes Annex I requirements. See Annex I sign-off over MCP. |
list_artifacts | read | Lists the 88 technical-file artifacts of a product, or the artifacts of one clause with clause (for example 6.2). Each row has the id, the clause, the type, the description, the stored status (missing when the product has no entry), generatedBy (null when a person wrote the text), the content length and the last update. It returns no content. exportGaps lists the open export gates. |
get_artifact | read | Returns the text of one technical-file artifact, its status (missing when the product has no entry), generatedBy and the last update. revision tells what save_artifact does with it. See Technical-file artifacts over MCP. |
save_artifact | products:write | Writes the text of one technical-file artifact as a draft for review. See Technical-file artifacts over MCP. Needs ADMIN. |
get_conformity_gaps | read | Lists open gaps across Annex I, technical documentation and evidence, and tells whether the technical file is ready. See Is the technical file ready?. An evidence gap on a requirement that is already signed off has signedOffWithoutEvidence: true. The product_context_missing gap names each empty Clause 6.2 input and the call that fills it. |
get_release_gate | read | Returns the CI/CD release gate verdict for one product, PASS or BLOCK, with the reason for each of the six checks. It stores nothing and leaves out the approver email. A PASS is not a statement of conformity. See Release gate. |
record_assessment_result | conformity:prepare or products:write | Records a dated assessment of one CRA clause. The acceptance rules of the mapping give the verdict. The tool has no verdict input. To change the verdict, add the evidence that the rules ask for and call it again. clauseRef takes the same forms as get_clause_mapping. |
upload_sbom | sbom:write | Stores an SBOM for one product, or for the whole company when productId is omitted, and replaces the stored one. With the optional part, for example firmware, it replaces only that part of the product SBOM. part needs productId. Accepts CycloneDX or SPDX JSON or a dependency manifest. Scans the components against OSV.dev and returns the CRA minimum-elements check. Needs an ADMIN and the Reporting plan. |
get_declaration_of_conformity | read | Builds the technical file with the Annex V Declaration of Conformity through the export gates. Returns the gaps when the file is not ready. ready: true means that the export gate is open. The gate is a minimum. doc.fileStatus tells whether the file is complete. Does not sign the declaration. |
Is the technical file ready?
Use readyForDocAssembly from get_conformity_gaps. It is true only when gaps is empty and the technical file of each product is complete as recorded.
gapslists the export gate, classification and Annex I evidence and sign-off checks, each with the call that closes it.fileStatushas one entry per product. It is the status thatget_declaration_of_conformityprints indoc.fileStatus, with every open gap in the file. These include Annex I applicability decisions, Article 13 duties, the Annex V and Annex VII points, and the machine drafts that a gate counts and no person has reviewed (idreview:<artifactId>, areadraft_review). Each gap has anactionand the producttabwhere you close it.- An incomplete file also adds one entry to
gapswithref: "technical_file_status". So an emptygapslist always means a complete file. - With no matching product,
readyForDocAssemblyisfalse.
get_declaration_of_conformity can return ready: true while doc.fileStatus says "Incomplete". The export gate opens on a minimum, for example one requirement signed off. get_compliance_status checks the company account only.
Background jobs
submit_job takes a type and an input object. The input must match the type. The platform checks the input when you submit the job. A bad input returns an error at once and queues no job.
| Type | Input | Result |
|---|---|---|
CLASSIFY_PRODUCT | productId, or text, or productName and description. With productId alone, the platform uses the stored product name and description. | A class suggestion. It does not change the product. Use set_product_classification to record the decision. |
IMPORT_PREFILL | documentContent (text, required), productId, optional documentName and documentRef | Extracts facts from the document and prefills the product assessment, each field with provenance. It drafts the Clause 6.2 inputs except C6.2-IN-03, which an agent writes with save_artifact. Send the text of a PDF, not the file. |
AUTO_ENRICH | productId (required) | Fills the fixed risk methodology, the SBOM entry, the issue log and the monitoring record. Lists candidate standards for the product category in candidateStandards. These are suggestions only. The job never records a standard as applied. Record the standards you applied in the product's Classification. vulnerabilitiesMonitoredCount is the number of components that the last complete vulnerability check covered. sbomCheck gives the component count, the number the check can query, and the reason when it covered fewer than all. The check asks OSV.dev about a component with a version and a package ecosystem, and NVD about a component with a CPE that has a version. It skips the others, for example a component whose package URL type is github or generic and that has no CPE. |
ASSEMBLE_DOSSIER | productId (required) | Assembles the technical dossier index and requests approval. |
DRAFT_ARTIFACT | artifactId and productId (required), optional inputs. Or a free-form prompt, or system with user. | Drafts one CRA artifact. |
Examples:
{ "type": "CLASSIFY_PRODUCT", "input": { "productId": "prod_123" } }
{ "type": "DRAFT_ARTIFACT", "input": { "artifactId": "C6.3-OUT-02", "productId": "prod_123" } }
{ "type": "IMPORT_PREFILL", "input": { "productId": "prod_123", "documentContent": "Section 1 ...", "documentName": "User manual" } }
POST /api/v1/jobs uses the same check and returns HTTP 400 for a bad input.
A job for a product that is not in the job's company fails with "Product not found in this workspace". It does not report success.
Filing with the ENISA Single Reporting Platform stays with a person. Tools prepare draft text and calculate deadlines. Authorized personnel review the draft, file on the SRP, and record the reference in the portal.
Tool annotations
Every tool on both servers sends the MCP annotations readOnlyHint, destructiveHint, idempotentHint and openWorldHint. Claude uses them to decide when to ask you. A read-only tool can run without a confirmation. A destructive tool always asks first.
| Annotation | Tools |
|---|---|
| Read-only. Changes no data | get_getting_started, get_compliance_status, generate_csaf_advisory, assess_response, get_next_deadline, get_next_task, get_job_status, ask_expert, get_conformity_gaps, get_release_gate, get_clause_mapping, lookup_regulatory_reference, list_products, generate_product_threats, propose_threat_treatment, get_threat_model, list_artifacts, get_artifact, get_declaration_of_conformity, list_submissions, get_submission, get_submission_journey, find_vendor_portal, track_vulnerability_report |
| Destructive. Can replace or remove data that exists | submit_job, set_product_classification, set_notified_body, save_threat_model, set_annex_i_evidence, set_annex_i_applicability, set_article13_duties, set_product_description_facts, set_support_period, save_artifact, signoff_annex_i_requirements, upload_sbom, acknowledge_submission, triage_submission, link_submission_product, record_article14_decision, prepare_article14_stage, assign_coordinator, record_remediation_decision, record_fix_verification, close_submission |
| Write that only adds data | submit_vulnerability, record_assessment_result, create_product, draft_reporter_message, request_advisory_publication, open_incident, submit_vulnerability_to_vendor, subscribe_to_report |
| Open world. Reaches outside your workspace | ask_expert, generate_product_threats, propose_threat_treatment and submit_job (AI models), upload_sbom (OSV.dev), acknowledge_submission, triage_submission and close_submission (email or status push to the reporter), find_vendor_portal, submit_vulnerability_to_vendor and subscribe_to_report (other manufacturers' portals and your callback URL) |
generate_product_threats and propose_threat_treatment need the products:write scope but save nothing, so they are read-only. A read tool that writes an audit log entry is still read-only.
Acting for a client company
A consultancy can act for a client company where the consultancy user holds a seat (a partner membership). Pass the client's company id as onBehalfOfCompanyId on any tool that reads or changes company data.
- Only a user-bound OAuth token can act for another company. A plain API key stays in its own company.
- A read needs a membership in the client company. A write needs ADMIN there.
- The membership is checked again on every call. When the client removes the seat, access stops at once.
- The plan check uses the client's plan, not the consultancy's. A refusal names the client and its plan, for example "This tool requires the sbom feature, and Acme is on the Free plan. Upgrade the plan of Acme."
- Background jobs run in the client company, so the job worker finds the client's products.
Without onBehalfOfCompanyId, every tool works in the company of the key or token. The company always comes from the verified credential and the membership check, never from a free tool argument.
Annex I sign-off over MCP
signoff_annex_i_requirements records a legal attestation in the technical file.
- The signatory is the person who owns the OAuth token. The platform takes the name and email from that user account, never from tool arguments.
- A plain API key cannot sign.
- The signatory must be ADMIN in the company the call acts for.
- Each signed entry records that it came through MCP and through which OAuth client. The audit log records the same.
- A requirement can be signed off with no evidence. The tool saves the sign-off and returns a line in
warningsfor each applicable requirement it signed with no evidence note and no linked evidence. Record the evidence withset_annex_i_evidence, or revoke the sign-off. - Until the evidence is recorded,
get_conformity_gapskeeps the evidence gap of that requirement open withsignedOffWithoutEvidence: true, and its summary counts these requirements. - A sign-off also counts as the review of evidence that an agent recorded with
set_annex_i_evidence, and closes its "Review the agent evidence" gap. Sign only after the signatory has read that evidence. A sign-off from before the evidence does not cover it, so sign the requirement again. - A revoke removes the sign-off and records who revoked it and when, in
lastRevokedBy,lastRevokedByEmailandlastRevokedAt. The technical file prints "Withdrawn by" with the name and the date on that row. A later sign-off keeps these fields. A revoke on a row that has no sign-off records nothing.
This differs from the dashboard. In the dashboard, a partner who drills into a client workspace cannot sign. Over MCP, a partner user who is ADMIN in the client company can sign for that client. Call the tool only when the signatory has reviewed the requirement and its evidence and has asked for the signature.
Applicability and Article 13 duties over MCP
The technical file asks for a decision on each Annex I Part I(2) requirement and a record for each Article 13 duty that CVD Portal does not record elsewhere. get_declaration_of_conformity lists each open one in doc.fileStatus, as "Decide whether Part I(2)(x) applies" and "Put Article 13(n) in place". Two tools close them. Read the keys and the current values with get_threat_model first, in annexIApplicability and article13Duties.
Both tools follow the provenance rules of the sign-off.
- Only a user-bound OAuth token can write. A plain API key cannot.
- The person must be ADMIN in the company the call acts for.
- The audit log records the change, the user, and the OAuth client as
mcp:<clientId>. The response gives the same value inrecordedVia. - A row you leave out stays as it is. A change made at the same time in the dashboard is not lost.
- A refused call writes nothing.
set_annex_i_applicability
Send a decision per letter, applicable or not_applicable, for example {"a":{"decision":"applicable"},"m":{"decision":"not_applicable","justification":"The kettle stores no user data or settings."}}.
not_applicableneeds ajustification. Article 13(4) requires the reason in the technical file. The tool refuses the decision without one.- Part I(1) and Part II always apply. The tool refuses a decision on them.
- The security objectives under a letter follow the decision in the same write. An objective that a person answered differently stays as it is.
objectivesUpdatedlists the objectives that changed. - A stored "does not apply" decision with no reason blocks the save, because the file cannot print it. The error names that letter. Send a decision with a justification for it in the same call.
- A letter that is signed off and that you mark
not_applicablekeeps its sign-off.warningsnames it.
set_article13_duties
Send a record per paragraph number, for example {"6":{"status":"in_place","evidence":"PSIRT-PROC-04 rev 2","owner":"PSIRT lead"}}.
| Field | Values |
|---|---|
status | not_started, in_progress, in_place or not_relied_on |
evidence | Where the evidence lives, for example a document id, a URL or a ticket. 1000 characters at most. null clears it |
owner | The person or team that owns the duty. null clears it |
in_placeneeds an evidence reference, sent in the same call or stored before.not_relied_onis only for 13(10) and 13(11), which apply only to a manufacturer that relies on them. The tool refuses it for any other paragraph.- A paragraph that CVD Portal records elsewhere, for example 13(12), takes no record. The tool refuses it and lists the paragraphs that do.
- A field you leave out stays as it is.
- A company duty (
scope: "company") is stored on each product, the same as in the dashboard. Record it for each product.warningsreminds you.
Annex VII facts over MCP
The technical documentation index marks each Annex VII point present or missing. get_declaration_of_conformity lists each missing point in doc.fileStatus as "Supply Annex VII point n". Most points rest on artifacts, which save_artifact drafts. Points 1 and 4 also need facts that only the manufacturer can state. Two tools record them.
| Point | What it needs | Over MCP |
|---|---|---|
| 1 | The product context (C6.2-OUT-01), the software versions, the hardware decision, photographs of a hardware product, and the user instructions (C7.5-OUT-04) | save_artifact for the two artifacts. set_product_description_facts for the versions and the hardware decision. Attach the photographs in the dashboard. |
| 2 | The architecture (C7.4-OUT-01), the SBOM and a contact address for vulnerability reports | save_artifact and upload_sbom. The contact address comes from the security contact email and the security.txt scan of the company. |
| 3 | The evaluated risks (C6.4-OUT-02) and the Annex I Part I(2) applicability table (C6.4-OUT-03) | save_artifact |
| 4 | How the support period was determined | set_support_period |
| 5 | The risk treatment decisions that describe the solutions adopted (C6.5-OUT-01) | save_artifact |
| 6 | The verification and validation results (C7.6-OUT-01) or an attached test report | save_artifact. Attach a test report in the dashboard. |
Both tools need a user-bound OAuth token and an ADMIN. The audit log records the change, the user, and the OAuth client as mcp:<clientId>. The response gives the same value in recordedVia. Record only facts and decisions that the manufacturer confirmed.
set_product_description_facts
placedOnMarketVersionlists the software versions that affect compliance with the essential requirements, for exampleapp 4.2.0, firmware 1.8.3. 300 characters at most.isHardwareistruefor a product with hardware andfalsefor software only.- A field you send replaces the stored value.
nullclears it. A field you leave out stays. photographsNeededin the response istruefor a hardware product. Point 1 stays open until image files of the external features, marking and internal layout are attached to C6.2-IN-05. No tool attaches them. Themissingtext of the point 1 gap infileStatusnames each open sub-point, the photographs included.
set_support_period
Send the period, the expected time in use and the basis, for example {"supportPeriodMonths":96,"expectedUseTimeMonths":96,"supportPeriodBasis":"Kettles of this class stay in use for about eight years.","placedOnMarketAt":"2026-03-01"}.
- The tool runs the same Article 13(8) checks as the dashboard. Five years is the floor, not a default.
- It refuses a period shorter than the expected time in use, a period under 60 months when the expected time in use is 60 months or more, and an implausible year of placing on the market. A refused call writes nothing.
factorsholds the evidence per Article 13(8) criterion. A factor you send replaces the stored one, and the others stay.placedOnMarketAtis the date of placing on the market,YYYY-MM-DD. The period runs from it, and the tool computes the end date. Left out, the stored date stays.warningsgives the checks that do not refuse the save, for examplestart_not_recordedwhen no date of placing on the market is known.
Technical-file artifacts over MCP
The technical file holds 88 artifacts, for example the six Clause 6.2 inputs that describe the product. list_artifacts shows which artifacts hold text, get_artifact returns the text of one, and save_artifact writes one.
save_artifactstores the text with statusneeds_reviewandgeneratedBy: "mcp". A person approves it in the dashboard with "Mark reviewed". The tool never setsverifiedand never writes a sign-off.- The export gate counts the draft at once. Until a person reviews it, the file status lists it as a gap, for example "Review the agent draft of C6.2-IN-03", so
readyForDocAssemblystaysfalse. Drafts fromIMPORT_PREFILLand other machine writers that a gate counts are listed the same way. An edit by a person in the dashboard does not close the gap. Only "Mark reviewed" does. - By default the tool writes only an empty artifact. With
overwrite: "machine"it also replaces text that a generator wrote, for exampleimport_prefillor an earliermcpdraft. - The tool never replaces text that a person wrote, or an artifact that a person approved. It returns an error that tells the agent to ask that person.
- The response has
exportGaps, the export gates that stay open after the save.
To revise a draft, read it with get_artifact first. Its revision field tells what save_artifact does with the artifact.
revision | Meaning |
|---|---|
{ "allowed": true, "overwrite": "empty_only" } | The artifact is empty. save_artifact writes it with the default overwrite. |
{ "allowed": true, "overwrite": "machine" } | A generator wrote the text. Send overwrite: "machine" to replace it. |
{ "allowed": false, "reason": "..." } | A person wrote or approved the text. save_artifact refuses. Ask that person to change it in the dashboard. |
The export gate product_context_missing waits for all six Clause 6.2 inputs, C6.2-IN-01 to C6.2-IN-06. The IMPORT_PREFILL job can draft five of them from a product description. No job drafts C6.2-IN-03 (market segments and the state of the art). An agent fills it with save_artifact:
{ "productId": "prod_123", "artifactId": "C6.2-IN-03", "content": "Market segment: consumer kitchen appliances in the EU. Comparable products: ..." }
Public MCP server
The researcher side needs no account and no API key. It exposes the same actions a researcher already performs in a browser, so an agent can find a vendor's portal, file a report, and follow it afterwards.
https://cvdportal.com/api/mcp/public
Claude Code
claude mcp add --transport http cvd-portal-public https://cvdportal.com/api/mcp/public
| Tool | What it does |
|---|---|
find_vendor_portal | Looks up a manufacturer's portal by slug or custom domain. Returns the portal URL, the security.txt URL, the policy URL, and the disclosure contact. |
submit_vulnerability_to_vendor | Files a report to a manufacturer's portal. Takes a slug plus the report fields. Returns a reference number and a tracking token. |
track_vulnerability_report | Reads the current status of a filed report from its tracking token. |
subscribe_to_report | Subscribes a public HTTPS callback URL to real-time status push notifications using the tracking token. Callbacks receive signed HTTP POST webhooks with status-only payloads. |
Three things are worth knowing before an agent uses this.
Record the tracking token. It is the only route back to an anonymous report and it cannot be recovered.
Filing is limited to 5 reports per minute, per portal, per IP, the same ceiling as the web form. The limit is shared, so switching between the form and MCP does not raise it.
track_vulnerability_report returns status, vendor, product, and dates. It never returns the report body, the reproduction steps, the impact, or the contact details. A tracking token proves possession, not authorship.
A disclosure is a permanent record on the vendor's side and cannot be retracted through the API. Confirm the vendor and the finding with the person you are acting for before filing.
Request limits
Both servers refuse an oversized request before they process it.
| Server | Largest request body | Largest tool input |
|---|---|---|
/api/mcp | 8 MiB | 2,000 array elements and object members |
/api/mcp/public | 256 KiB | 200 array elements and object members |
A body over the limit gets HTTP 413. A tool input over the limit gets an error result. An SBOM sent to upload_sbom is one text value, so its size counts against the body limit only. upload_sbom takes up to 5 MB of SBOM text.
Privacy
Tool-call analytics record which tool ran, its outcome, and timing. Argument and result content, which can include vulnerability details, is not recorded by default. This applies to both servers.
On the manufacturer server, each call is linked to the internal id of the company and, for a browser sign-in, of the user. No name or email address is sent. Calls to the researcher server stay anonymous.