#
tokens: 18960/50000 20/20 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── ci.yml
│       ├── release.yml
│       └── security.yml
├── .gitignore
├── .golangci.yaml
├── .ko.yaml
├── cmd
│   └── server
│       ├── main_test.go
│       └── main.go
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── dco.md
├── go.mod
├── go.sum
├── LICENSE
├── pkg
│   ├── mcp
│   │   ├── server_test.go
│   │   └── server.go
│   └── osv
│       ├── client_test.go
│       └── client.go
├── README.md
├── renovate.json
├── SECURITY.md
└── Taskfile.yml
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

# Build artifacts
/build/

# IDE files
.idea/
.vscode/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# ko build artifacts
.ko.local/

# task
.task

```

--------------------------------------------------------------------------------
/.ko.yaml:
--------------------------------------------------------------------------------

```yaml
builds:
- id: osv-mcp-server
  dir: .
  main: ./cmd/server
  env:
    - CGO_ENABLED=0
  flags:
    - -trimpath
  ldflags:
    - -s -w
    - -X main.version={{.Env.VERSION}}
  labels:
    org.opencontainers.image.created: "{{.Env.CREATION_TIME}}"
    org.opencontainers.image.description: "osv-mcp - An OSV MCP Server"
    org.opencontainers.image.licenses: "Apache-2.0"
    org.opencontainers.image.revision: "{{.Env.GITHUB_SHA}}"
    org.opencontainers.image.source: "{{.Env.GITHUB_SERVER_URL}}/{{.Env.GITHUB_REPOSITORY}}"
    org.opencontainers.image.title: "osv-mcp"
    org.opencontainers.image.url: "{{.Env.GITHUB_SERVER_URL}}/{{.Env.GITHUB_REPOSITORY}}"
    org.opencontainers.image.version: "{{.Env.VERSION}}"

```

--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------

```yaml
version: "2"
run:
  issues-exit-code: 1
output:
  formats:
    text:
      path: stdout
      print-linter-name: true
      print-issued-lines: true
linters:
  default: none
  enable:
    - depguard
    - exhaustive
    - goconst
    - gocyclo
    - gosec
    - govet
    - ineffassign
    - lll
    - paralleltest
    - promlinter
    - revive
    - staticcheck
    - thelper
    - tparallel
    - unparam
    - unused
  settings:
    depguard:
      rules:
        prevent_unmaintained_packages:
          list-mode: lax
          files:
            - $all
            - '!$test'
          deny:
            - pkg: io/ioutil
              desc: this is deprecated
    gocyclo:
      min-complexity: 15
    gosec:
      excludes:
        - G601
    lll:
      line-length: 130
    revive:
      severity: warning
      rules:
        - name: blank-imports
          severity: warning
        - name: context-as-argument
        - name: context-keys-type
        - name: duplicated-imports
        - name: error-naming
        - name: error-return
        - name: exported
          severity: error
        - name: if-return
        - name: identical-branches
        - name: indent-error-flow
        - name: import-shadowing
        - name: package-comments
        - name: redefines-builtin-id
        - name: struct-tag
        - name: unconditional-recursion
        - name: unnecessary-stmt
        - name: unreachable-code
        - name: unused-parameter
        - name: unused-receiver
        - name: unhandled-error
          disabled: true
  exclusions:
    generated: lax
    rules:
      - linters:
          - lll
          - gocyclo
          - errcheck
          - dupl
          - gosec
          - paralleltest
        path: (.+)_test\.go
      - linters:
          - lll
        path: .golangci.yml
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gci
    - gofmt
  settings:
    gci:
      sections:
        - standard
        - default
        - prefix(github.com/StacklokLabs/osv-mcp)
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# OSV MCP Server
[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/StacklokLabs/osv-mcp)](https://archestra.ai/mcp-catalog/stackloklabs__osv-mcp)

An MCP (Model Context Protocol) server that provides access to the
[OSV (Open Source Vulnerabilities) database](https://osv.dev/).

## Overview

This project implements an SSE-based MCP server that allows LLM-powered
applications to query the OSV database for vulnerability information. The server
provides tools for:

1. Querying vulnerabilities for a specific package version or commit
2. Batch querying vulnerabilities for multiple packages or commits
3. Getting detailed information about a specific vulnerability by ID

## Installation

### Prerequisites

- Go 1.21 or later
- [Task](https://taskfile.dev/) (optional, for running tasks)
- [ko](https://ko.build/) (optional, for building container images)

### Building from source

```bash
# Clone the repository
git clone https://github.com/StacklokLabs/osv-mcp.git
cd osv-mcp

# Build the server
task build
```

## Usage

### Running with ToolHive (Recommended)

The easiest way to run the OSV MCP server is using
[ToolHive](https://github.com/stacklok/toolhive), which provides secure,
containerized deployment of MCP servers:

```bash
# Install ToolHive (if not already installed)
# See: https://docs.stacklok.com/toolhive/guides-cli/install

# Register a supported client so ToolHive can auto-configure your environment
thv client setup

# Run the OSV MCP server (packaged as 'osv' in ToolHive)
thv run osv

# List running servers
thv list

# Get detailed information about the server
thv registry info osv
```

The server will be available to your MCP-compatible clients and can query the
OSV database for vulnerability information.

### Running from Source

### Server Configuration

The server can be configured using environment variables:

- `MCP_PORT`: The port number to run the server on (default: 8080)

  - Must be a valid integer between 0 and 65535
  - If invalid or not set, the server will use port 8080

- `MCP_TRANSPORT`: The transport mode for the server (default: `sse`)
  - Supported values: `sse`, `streamable-http`
  - If invalid or not set, the server will use SSE transport mode

Example:

```bash
# Run on port 3000
MCP_PORT=3000 ./build/osv-mcp-server

# Run on default port 8080
./build/osv-mcp-server
```

### MCP Tools

The server provides the following MCP tools:

#### query_vulnerability

Query for vulnerabilities affecting a specific package version or commit.

**Input Schema:**

```json
{
  "type": "object",
  "properties": {
    "commit": {
      "type": "string",
      "description": "The commit hash to query for. If specified, version should not be set."
    },
    "version": {
      "type": "string",
      "description": "The version string to query for. If specified, commit should not be set."
    },
    "package_name": {
      "type": "string",
      "description": "The name of the package."
    },
    "ecosystem": {
      "type": "string",
      "description": "The ecosystem for this package (e.g., PyPI, npm, Go)."
    },
    "purl": {
      "type": "string",
      "description": "The package URL for this package. If purl is used, package_name and ecosystem should not be set."
    }
  }
}
```

#### query_vulnerabilities_batch

Query for vulnerabilities affecting multiple packages or commits at once.

**Input Schema:**

```json
{
  "type": "object",
  "properties": {
    "queries": {
      "type": "array",
      "description": "Array of query objects",
      "items": {
        "type": "object",
        "properties": {
          "commit": {
            "type": "string",
            "description": "The commit hash to query for. If specified, version should not be set."
          },
          "version": {
            "type": "string",
            "description": "The version string to query for. If specified, commit should not be set."
          },
          "package_name": {
            "type": "string",
            "description": "The name of the package."
          },
          "ecosystem": {
            "type": "string",
            "description": "The ecosystem for this package (e.g., PyPI, npm, Go)."
          },
          "purl": {
            "type": "string",
            "description": "The package URL for this package. If purl is used, package_name and ecosystem should not be set."
          }
        }
      }
    }
  },
  "required": ["queries"]
}
```

#### get_vulnerability

Get details for a specific vulnerability by ID.

**Input Schema:**

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "The OSV vulnerability ID"
    }
  },
  "required": ["id"]
}
```

## Examples

### Querying vulnerabilities for a package

```json
{
  "package_name": "lodash",
  "ecosystem": "npm",
  "version": "4.17.15"
}
```

### Querying vulnerabilities for a commit

```json
{
  "commit": "6879efc2c1596d11a6a6ad296f80063b558d5e0f"
}
```

### Batch querying vulnerabilities

```json
{
  "queries": [
    {
      "package_name": "lodash",
      "ecosystem": "npm",
      "version": "4.17.15"
    },
    {
      "package_name": "jinja2",
      "ecosystem": "PyPI",
      "version": "2.4.1"
    }
  ]
}
```

### Getting vulnerability details

```json
{
  "id": "GHSA-vqj2-4v8m-8vrq"
}
```

## Development

### Running tests

```bash
task test
```

### Linting

```bash
task lint
```

### Formatting code

```bash
task fmt
```

## Contributing

We welcome contributions to this MCP server! If you'd like to contribute, please
review the [CONTRIBUTING guide](./CONTRIBUTING.md) for details on how to get
started.

If you run into a bug or have a feature request, please
[open an issue](https://github.com/StacklokLabs/osv-mcp/issues) in the
repository or join us in the `#mcp-servers` channel on our
[community Discord server](https://discord.gg/stacklok).

## License

This project is licensed under the Apache v2 License - see the LICENSE file for
details.

```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
# Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
  advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <[email protected]>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to `osv-mcp` <!-- omit from toc -->

First off, thank you for taking the time to contribute to osv-mcp! :+1: :tada:
osv-mcp is released under the Apache 2.0 license. If you would like to
contribute something or want to hack on the code, this document should help you
get started. You can find some hints for starting development in osv-mcp's
[README](https://github.com/StacklokLabs/osv-mcp/blob/main/README.md).

## Table of contents <!-- omit from toc -->

- [Code of conduct](#code-of-conduct)
- [Reporting security vulnerabilities](#reporting-security-vulnerabilities)
- [How to contribute](#how-to-contribute)
  - [Using GitHub Issues](#using-github-issues)
  - [Not sure how to start contributing?](#not-sure-how-to-start-contributing)
  - [Pull request process](#pull-request-process)
  - [Commit message guidelines](#commit-message-guidelines)

## Code of conduct

This project adheres to the
[Contributor Covenant](https://github.com/StacklokLabs/osv-mcp/blob/main/CODE_OF_CONDUCT.md)
code of conduct. By participating, you are expected to uphold this code. Please
report unacceptable behavior to
[[email protected]](mailto:[email protected]).

## Reporting security vulnerabilities

If you think you have found a security vulnerability in osv-mcp please DO NOT
disclose it publicly until we've had a chance to fix it. Please don't report
security vulnerabilities using GitHub issues; instead, please follow this
[process](https://github.com/StacklokLabs/osv-mcp/blob/main/SECURITY.md)

## How to contribute

### Using GitHub Issues

We use GitHub issues to track bugs and enhancements. If you have a general usage
question, please ask in the #mcp-servers channel of the
[Stacklok Discord server](https://discord.gg/stacklok).

If you are reporting a bug, please help to speed up problem diagnosis by
providing as much information as possible. Ideally, that would include a small
sample project that reproduces the problem.

### Not sure how to start contributing?

PRs to resolve existing issues are greatly appreciated and issues labeled as
["good first issue"](https://github.com/StacklokLabs/osv-mcp/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
are a great place to start!

### Pull request process

-All commits must include a Signed-off-by trailer at the end of each commit
message to indicate that the contributor agrees to the Developer Certificate of
Origin. For additional details, check out the [DCO instructions](dco.md).

- Create an issue outlining the fix or feature.
- Fork the osv-mcp repository to your own GitHub account and clone it locally.
- Hack on your changes.
- Correctly format your commit messages, see
  [Commit message guidelines](#commit-message-guidelines) below.
- Open a PR by ensuring the title and its description reflect the content of the
  PR.
- Ensure that CI passes, if it fails, fix the failures.
- Every pull request requires a review from the core osv-mcp team before
  merging.
- Once approved, all of your commits will be squashed into a single commit with
  your PR title.

### Commit message guidelines

We follow the commit formatting recommendations found on
[Chris Beams' How to Write a Git Commit Message article](https://chris.beams.io/posts/git-commit/):

1. Separate subject from body with a blank line
1. Limit the subject line to 50 characters
1. Capitalize the subject line
1. Do not end the subject line with a period
1. Use the imperative mood in the subject line
1. Use the body to explain what and why vs. how

```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
# Security Policy

The StacklokLabs community take security seriously! We appreciate your efforts
to disclose your findings responsibly and will make every effort to acknowledge
your contributions.

## Reporting a vulnerability

To report a security issue, please use the GitHub Security Advisory
["Report a Vulnerability"](https://github.com/StacklokLabs/osv-mcp/security/advisories/new)
tab.

If you are unable to access GitHub you can also email us at
[[email protected]](mailto:[email protected]).

Include steps to reproduce the vulnerability, the vulnerable versions, and any
additional files to reproduce the vulnerability.

If you are only comfortable sharing under GPG, please start by sending an email
requesting a public PGP key to use for encryption.

### Contacting the StacklokLabs security team

Contact the team by sending email to
[[email protected]](mailto:[email protected]).

## Disclosures

### Private disclosure processes

The StacklokLabs community asks that all suspected vulnerabilities be handled in
accordance with
[Responsible Disclosure model](https://en.wikipedia.org/wiki/Responsible_disclosure).

### Public disclosure processes

If anyone knows of a publicly disclosed security vulnerability please
IMMEDIATELY email [[email protected]](mailto:[email protected]) to
inform us about the vulnerability so that we may start the patch, release, and
communication process.

If a reporter contacts the us to express intent to make an issue public before a
fix is available, we will request if the issue can be handled via a private
disclosure process. If the reporter denies the request, we will move swiftly
with the fix and release process.

## Patch, release, and public communication

For each vulnerability, the StacklokLabs security team will coordinate to create
the fix and release, and notify the rest of the community.

All of the timelines below are suggestions and assume a Private Disclosure.

- The security team drives the schedule using their best judgment based on
  severity, development time, and release work.
- If the security team is dealing with a Public Disclosure all timelines become
  ASAP.
- If the fix relies on another upstream project's disclosure timeline, that will
  adjust the process as well.
- We will work with the upstream project to fit their timeline and best protect
  StacklokLabs users.
- The Security team will give advance notice to the Private Distributors list
  before the fix is released.

### Fix team organization

These steps should be completed within the first 24 hours of Disclosure.

- The security team will work quickly to identify relevant engineers from the
  affected projects and packages and being those engineers into the
  [security advisory](https://docs.github.com/en/code-security/security-advisories/)
  thread.
- These selected developers become the "Fix Team" (the fix team is often drawn
  from the projects MAINTAINERS)

### Fix development process

These steps should be completed within the 1-7 days of Disclosure.

- Create a new
  [security advisory](https://docs.github.com/en/code-security/security-advisories/)
  in affected repository by visiting
  `https://github.com/StacklokLabs/osv-mcp/security/advisories/new`
- As many details as possible should be entered such as versions affected, CVE
  (if available yet). As more information is discovered, edit and update the
  advisory accordingly.
- Use the CVSS calculator to score a severity level.
- Add collaborators from codeowners team only (outside members can only be added
  after approval from the security team)
- The reporter may be added to the issue to assist with review, but **only
  reporters who have contacted the security team using a private channel**.
- Select 'Request CVE'
- The security team / Fix Team create a private temporary fork
- The Fix team performs all work in a 'security advisory' within its temporary
  fork
- CI can be checked locally using the [act](https://github.com/nektos/act)
  project
- All communication happens within the security advisory, it is _not_ discussed
  in slack channels or non private issues.
- The Fix Team will notify the security team that work on the fix branch is
  completed, this can be done by tagging names in the advisory
- The Fix team and the security team will agree on fix release day
- The recommended release time is 4pm UTC on a non-Friday weekday. This means
  the announcement will be seen morning Pacific, early evening Europe, and late
  evening Asia.

If the CVSS score is under ~4.0
([a low severity score](https://www.first.org/cvss/specification-document#i5))
or the assessed risk is low the Fix Team can decide to slow the release process
down in the face of holidays, developer bandwidth, etc.

Note: CVSS is convenient but imperfect. Ultimately, the security team has
discretion on classifying the severity of a vulnerability.

The severity of the bug and related handling decisions must be discussed on in
the security advisory, never in public repos.

### Fix disclosure process

With the Fix Development underway, the security team needs to come up with an
overall communication plan for the wider community. This Disclosure process
should begin after the Fix Team has developed a Fix or mitigation so that a
realistic timeline can be communicated to users.

**Fix release day** (Completed within 1-21 days of Disclosure)

- The Fix Team will approve the related pull requests in the private temporary
  branch of the security advisory
- The security team will merge the security advisory / temporary fork and its
  commits into the main branch of the affected repository
- The security team will ensure all the binaries are built, signed, publicly
  available, and functional.
- The security team will announce the new releases, the CVE number, severity,
  and impact, and the location of the binaries to get wide distribution and user
  action. As much as possible this announcement should be actionable, and
  include any mitigating steps users can take prior to upgrading to a fixed
  version. An announcement template is available below. The announcement will be
  sent to the following channels:
- A link to fix will be posted to the
  [Stacklok Discord Server](https://discord.gg/stacklok) in the #mcp-servers
  channel.

## Retrospective

These steps should be completed 1-3 days after the Release Date. The
retrospective process
[should be blameless](https://landing.google.com/sre/book/chapters/postmortem-culture.html).

- The security team will send a retrospective of the process to the
  [Stacklok Discord Server](https://discord.gg/stacklok) including details on
  everyone involved, the timeline of the process, links to relevant PRs that
  introduced the issue, if relevant, and any critiques of the response and
  release process.

```

--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ]
}

```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
version: 2
updates:
  # Enable version updates for Go modules
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

  # Enable version updates for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version-file: 'go.mod'
          cache: true

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v8
        with:
          version: latest

  build:
    name: Build and Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version-file: 'go.mod'
          cache: true

      - name: Install Task
        uses: arduino/setup-task@v2
        with:
          version: '3.x'
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install dependencies
        run: task install

      - name: Build
        run: task build

      - name: Test
        run: task test

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: osv-mcp-server
          path: build/osv-mcp-server
          retention-days: 7
```

--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------

```yaml
name: Security Scan

on:
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * 0'  # Run weekly on Sundays at midnight

jobs:
  trivy-scan:
    name: Trivy Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Run Trivy vulnerability scanner in repo mode
        uses: aquasecurity/[email protected]
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
          category: 'trivy-fs'

      - name: Run Trivy vulnerability scanner in IaC mode
        uses: aquasecurity/[email protected]
        with:
          scan-type: 'config'
          hide-progress: false
          format: 'sarif'
          output: 'trivy-config-results.sarif'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy IaC scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: 'trivy-config-results.sarif'
          category: 'trivy-config'
```

--------------------------------------------------------------------------------
/cmd/server/main_test.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/StacklokLabs/osv-mcp/pkg/mcp"
	"github.com/StacklokLabs/osv-mcp/pkg/osv"
)

func TestCreateServer(t *testing.T) {
	// Create OSV client
	osvClient := osv.NewClient()
	require.NotNil(t, osvClient)

	// Create MCP server
	mcpServer := mcp.NewServer(
		mcp.WithOSVClient(osvClient),
	)
	require.NotNil(t, mcpServer)

	// Verify server properties
	assert.Equal(t, mcp.ServerName, "osv-mcp")
	assert.Equal(t, mcp.ServerVersion, "0.1.0")
}

func TestGetMCPServerPort(t *testing.T) {
	// Save original env value and restore it after the test
	originalPort := os.Getenv("MCP_PORT")
	defer func() {
		if originalPort != "" {
			os.Setenv("MCP_PORT", originalPort)
		} else {
			os.Unsetenv("MCP_PORT")
		}
	}()

	tests := []struct {
		name     string
		envPort  string
		expected string
	}{
		{
			name:     "No environment variable set",
			envPort:  "",
			expected: "8080",
		},
		{
			name:     "Valid port number",
			envPort:  "3000",
			expected: "3000",
		},
		{
			name:     "Invalid port (non-numeric)",
			envPort:  "abc",
			expected: "8080",
		},
		{
			name:     "Invalid port (negative number)",
			envPort:  "-1",
			expected: "8080",
		},
		{
			name:     "Invalid port (too large)",
			envPort:  "70000",
			expected: "8080",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Set up environment
			if tt.envPort != "" {
				os.Setenv("MCP_PORT", tt.envPort)
			} else {
				os.Unsetenv("MCP_PORT")
			}

			// Test the function
			port := getMCPServerPort()
			assert.Equal(t, tt.expected, port)
		})
	}
}

```

--------------------------------------------------------------------------------
/Taskfile.yml:
--------------------------------------------------------------------------------

```yaml
version: '3'

vars:
  BINARY_NAME: osv-mcp-server
  BUILD_DIR: build
  MAIN_PACKAGE: ./cmd/server
  KO_DOCKER_REPO: stackloklabs/osv-mcp

tasks:
  default:
    desc: Run tests and build the application
    deps: [test, build]

  build:
    desc: Build the application
    cmds:
      - mkdir -p {{.BUILD_DIR}}
      - go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}

  run:
    desc: Run the application
    deps: [build]
    cmds:
      - ./{{.BUILD_DIR}}/{{.BINARY_NAME}} {{.CLI_ARGS}}

  test:
    desc: Run tests
    cmds:
      - go test -v ./...

  clean:
    desc: Clean the build directory
    cmds:
      - rm -rf {{.BUILD_DIR}}

  fmt:
    desc: Format the code
    cmds:
      - go fmt ./...
      - golangci-lint run --fix

  lint:
    desc: Lint the code
    cmds:
      - golangci-lint run

  deps:
    desc: Update dependencies
    cmds:
      - go mod tidy

  install:
    desc: Install dependencies
    cmds:
      - go mod download

  ko-build:
    desc: Build container image with ko
    env:
      KO_DOCKER_REPO: '{{.KO_DOCKER_REPO}}'
    cmds:
      - ko build --platform=linux/amd64 --base-import-paths {{.MAIN_PACKAGE}} -t latest

  ko-run:
    desc: Run container built with ko
    env:
      KO_DOCKER_REPO: '{{.KO_DOCKER_REPO}}'
    cmds:
      - ko run --platform=linux/amd64 --base-import-paths {{.MAIN_PACKAGE}} -- {{.CLI_ARGS}}

  ko-publish:
    desc: Publish container image with ko
    env:
      KO_DOCKER_REPO: '{{.KO_DOCKER_REPO}}'
    cmds:
      - ko publish --platform=linux/amd64 --base-import-paths {{.MAIN_PACKAGE}} -t latest

  all:
    desc: Run all tasks (fmt, lint, test, build)
    cmds:
      - task: fmt
      - task: lint
      - task: test
      - task: build
```

--------------------------------------------------------------------------------
/dco.md:
--------------------------------------------------------------------------------

```markdown
# Developer Certificate of Origin (DCO)

In order to contribute to the project, you must agree to the Developer
Certificate of Origin. A
[Developer Certificate of Origin (DCO)](https://developercertificate.org/) is an
affirmation that the developer contributing the proposed changes has the
necessary rights to submit those changes. A DCO provides some additional legal
protections while being relatively easy to do.

The entire DCO can be summarized as:

- Certify that the submitted code can be submitted under the open source license
  of the project (e.g. MIT)
- I understand that what I am contributing is public and will be redistributed
  indefinitely

## How to Use Developer Certificate of Origin

In order to contribute to the project, you must agree to the Developer
Certificate of Origin. To confirm that you agree, your commit message must
include a Signed-off-by trailer at the bottom of the commit message.

For example, it might look like the following:

```bash
A commit message

Closes gh-345

Signed-off-by: jane marmot <[email protected]>
```

The Signed-off-by [trailer](https://git-scm.com/docs/git-interpret-trailers) can
be added automatically by using the
[-s or –signoff command line option](https://git-scm.com/docs/git-commit/2.13.7#Documentation/git-commit.txt--s)
when specifying your commit message:

```bash
git commit -s -m
```

If you have chosen the
[Keep my email address private](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address#about-commit-email-addresses)
option within GitHub, the Signed-off-by trailer might look something like:

```bash
A commit message

Closes gh-345

Signed-off-by: jane marmot <[email protected]>
```

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    name: Release Container
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version-file: 'go.mod'
          cache: true

      - name: Install Task
        uses: arduino/setup-task@v2
        with:
          version: '3.x'
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install dependencies
        run: task install

      - name: Test
        run: task test

      - name: Setup Ko
        uses: ko-build/[email protected]

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract tag version
        id: tag
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Set repository owner lowercase
        id: repo_owner
        run: echo "OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

      - name: Build and push container
        env:
          KO_DOCKER_REPO: ghcr.io/${{ steps.repo_owner.outputs.OWNER }}/osv-mcp
          VERSION: ${{ steps.tag.outputs.VERSION }}
          CREATION_TIME: $(date -u +'%Y-%m-%dT%H:%M:%SZ')
        run: |
          # Build and push the container with reproducible build flags
          ko build \
            --bare \
            --sbom=spdx \
            --platform=linux/amd64,linux/arm64 \
            --base-import-paths \
            --tags $VERSION,latest \
            ./cmd/server

      - name: Install Cosign
        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0

      - name: Sign Image with Cosign
        env:
          KO_DOCKER_REPO: ghcr.io/${{ steps.repo_owner.outputs.OWNER }}/osv-mcp
        run: |
          TAG=$(echo "${{ steps.tag.outputs.VERSION }}" | sed 's/+/_/g')
          # Sign the ko image
          cosign sign -y $KO_DOCKER_REPO/server:$TAG

          # Sign the latest tag if building from a tag
          if [[ "${{ github.ref }}" == refs/tags/* ]]; then
            cosign sign -y $KO_DOCKER_REPO/server:latest
          fi

```

--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------

```go
// Package main provides the entry point for the OSV MCP Server.
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"

	"github.com/StacklokLabs/osv-mcp/pkg/mcp"
	"github.com/StacklokLabs/osv-mcp/pkg/osv"
)

// TransportMode defines the type for transport modes used by the MCP server.
type TransportMode string

const (
	// TransportSSE represents the Server-Sent Events transport mode.
	TransportSSE TransportMode = "sse"
	// TransportHTTPStream represents the HTTP streaming transport mode.
	TransportHTTPStream TransportMode = "streamable-http"
)

func getTransportMode() (TransportMode, error) {
	mode := strings.ToLower(strings.TrimSpace(os.Getenv("MCP_TRANSPORT")))
	if mode == "" {
		return TransportSSE, nil // default
	}

	switch TransportMode(mode) {
	case TransportSSE, TransportHTTPStream:
		return TransportMode(mode), nil
	default:
		return "", fmt.Errorf("invalid MCP_TRANSPORT: %q (allowed: sse, streamable-http)", mode)
	}
}

// getMCPServerPort returns the port number from MCP_PORT environment variable.
// If the environment variable is not set or contains an invalid value,
// it returns the default port 8080.
func getMCPServerPort() string {
	port := "8080"
	if envPort := os.Getenv("MCP_PORT"); envPort != "" {
		if portNum, err := strconv.Atoi(envPort); err == nil {
			if portNum >= 0 && portNum <= 65535 {
				port = envPort
			} else {
				log.Printf("Invalid MCP_PORT value: %s (must be between 0 and 65535), using default port 8080", envPort)
			}
		} else {
			log.Printf("Invalid MCP_PORT value: %s (must be a valid number), using default port 8080", envPort)
		}
	}
	return port
}

func main() {
	// Get port from environment variable or use default
	port := getMCPServerPort()

	// Parse command-line flags
	addr := flag.String("addr", ":"+port, "Address to listen on")
	flag.Parse()

	mode, err := getTransportMode()
	if err != nil {
		log.Fatalf("Error getting transport mode: %v", err)
	}

	// Create OSV client
	osvClient := osv.NewClient()

	// Create MCP server
	mcpServer := mcp.NewServer(
		mcp.WithOSVClient(osvClient),
	)

	// Handle signals for graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	// Start server in a goroutine
	errChan := make(chan error, 1)
	go func() {
		switch mode {
		case TransportHTTPStream:
			errChan <- mcpServer.ServeHTTPStream(*addr)
		case TransportSSE:
			errChan <- mcpServer.ServeSSE(*addr)
		}
	}()

	// Wait for signal or error
	select {
	case err := <-errChan:
		if err != nil {
			log.Fatalf("Server error: %v", err)
		}
	case sig := <-sigChan:
		log.Printf("Received signal: %v", sig)
	}

	log.Println("Shutting down server")
}

```

--------------------------------------------------------------------------------
/pkg/osv/client_test.go:
--------------------------------------------------------------------------------

```go
package osv

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestQuery(t *testing.T) {
	// Setup test server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check request method and path
		assert.Equal(t, http.MethodPost, r.Method)
		assert.Equal(t, "/v1/query", r.URL.Path)
		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))

		// Decode request body
		var req QueryRequest
		err := json.NewDecoder(r.Body).Decode(&req)
		require.NoError(t, err)

		// Check request fields
		assert.Equal(t, "test-package", req.Package.Name)
		assert.Equal(t, "npm", req.Package.Ecosystem)
		assert.Equal(t, "1.0.0", req.Version)

		// Return mock response
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		response := `{
			"vulns": [
				{
					"id": "TEST-2023-001",
					"summary": "Test vulnerability",
					"details": "This is a test vulnerability",
					"modified": "2023-01-01T00:00:00Z",
					"published": "2023-01-01T00:00:00Z",
					"references": [
						{
							"type": "ADVISORY",
							"url": "https://example.com/advisory/TEST-2023-001"
						}
					],
					"affected": [
						{
							"package": {
								"name": "test-package",
								"ecosystem": "npm"
							},
							"ranges": [
								{
									"type": "SEMVER",
									"events": [
										{
											"introduced": "0"
										},
										{
											"fixed": "1.0.1"
										}
									]
								}
							],
							"versions": ["1.0.0"]
						}
					]
				}
			]
		}`
		_, _ = w.Write([]byte(response))
	}))
	defer server.Close()

	// Create client with test server URL
	client := NewClient(WithBaseURL(server.URL + "/v1"))

	// Create query request
	req := QueryRequest{
		Version: "1.0.0",
		Package: Package{
			Name:      "test-package",
			Ecosystem: "npm",
		},
	}

	// Execute query
	resp, err := client.Query(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, resp)

	// Check response
	assert.Len(t, resp.Vulns, 1)
	vuln := resp.Vulns[0]
	assert.Equal(t, "TEST-2023-001", vuln.ID)
	assert.Equal(t, "Test vulnerability", vuln.Summary)
	assert.Equal(t, "This is a test vulnerability", vuln.Details)

	expectedModified, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z")
	assert.Equal(t, expectedModified, vuln.Modified)

	assert.Len(t, vuln.References, 1)
	assert.Equal(t, "ADVISORY", vuln.References[0].Type)
	assert.Equal(t, "https://example.com/advisory/TEST-2023-001", vuln.References[0].URL)

	assert.Len(t, vuln.Affected, 1)
	assert.Equal(t, "test-package", vuln.Affected[0].Package.Name)
	assert.Equal(t, "npm", vuln.Affected[0].Package.Ecosystem)
}

func TestQueryBatch(t *testing.T) {
	// Setup test server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check request method and path
		assert.Equal(t, http.MethodPost, r.Method)
		assert.Equal(t, "/v1/querybatch", r.URL.Path)
		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))

		// Decode request body
		var req QueryBatchRequest
		err := json.NewDecoder(r.Body).Decode(&req)
		require.NoError(t, err)

		// Check request fields
		assert.Len(t, req.Queries, 2)

		// Return mock response
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		response := `{
			"results": [
				{
					"vulns": [
						{
							"id": "TEST-2023-001",
							"modified": "2023-01-01T00:00:00Z"
						}
					]
				},
				{
					"vulns": [
						{
							"id": "TEST-2023-002",
							"modified": "2023-01-02T00:00:00Z"
						}
					]
				}
			]
		}`
		_, _ = w.Write([]byte(response))
	}))
	defer server.Close()

	// Create client with test server URL
	client := NewClient(WithBaseURL(server.URL + "/v1"))

	// Create batch query request
	req := QueryBatchRequest{
		Queries: []QueryRequest{
			{
				Version: "1.0.0",
				Package: Package{
					Name:      "test-package-1",
					Ecosystem: "npm",
				},
			},
			{
				Version: "2.0.0",
				Package: Package{
					Name:      "test-package-2",
					Ecosystem: "npm",
				},
			},
		},
	}

	// Execute batch query
	resp, err := client.QueryBatch(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, resp)

	// Check response
	assert.Len(t, resp.Results, 2)

	assert.Len(t, resp.Results[0].Vulns, 1)
	assert.Equal(t, "TEST-2023-001", resp.Results[0].Vulns[0].ID)

	expectedModified1, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z")
	assert.Equal(t, expectedModified1, resp.Results[0].Vulns[0].Modified)

	assert.Len(t, resp.Results[1].Vulns, 1)
	assert.Equal(t, "TEST-2023-002", resp.Results[1].Vulns[0].ID)

	expectedModified2, _ := time.Parse(time.RFC3339, "2023-01-02T00:00:00Z")
	assert.Equal(t, expectedModified2, resp.Results[1].Vulns[0].Modified)
}

func TestGetVulnerability(t *testing.T) {
	// Setup test server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check request method and path
		assert.Equal(t, http.MethodGet, r.Method)
		assert.Equal(t, "/v1/vulns/TEST-2023-001", r.URL.Path)

		// Return mock response
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		response := `{
			"id": "TEST-2023-001",
			"summary": "Test vulnerability",
			"details": "This is a test vulnerability",
			"modified": "2023-01-01T00:00:00Z",
			"published": "2023-01-01T00:00:00Z",
			"references": [
				{
					"type": "ADVISORY",
					"url": "https://example.com/advisory/TEST-2023-001"
				}
			],
			"affected": [
				{
					"package": {
						"name": "test-package",
						"ecosystem": "npm"
					},
					"ranges": [
						{
							"type": "SEMVER",
							"events": [
								{
									"introduced": "0"
								},
								{
									"fixed": "1.0.1"
								}
							]
						}
					],
					"versions": ["1.0.0"]
				}
			]
		}`
		_, _ = w.Write([]byte(response))
	}))
	defer server.Close()

	// Create client with test server URL
	client := NewClient(WithBaseURL(server.URL + "/v1"))

	// Execute get vulnerability
	vuln, err := client.GetVulnerability(context.Background(), "TEST-2023-001")
	require.NoError(t, err)
	require.NotNil(t, vuln)

	// Check response
	assert.Equal(t, "TEST-2023-001", vuln.ID)
	assert.Equal(t, "Test vulnerability", vuln.Summary)
	assert.Equal(t, "This is a test vulnerability", vuln.Details)

	expectedModified, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z")
	assert.Equal(t, expectedModified, vuln.Modified)

	assert.Len(t, vuln.References, 1)
	assert.Equal(t, "ADVISORY", vuln.References[0].Type)
	assert.Equal(t, "https://example.com/advisory/TEST-2023-001", vuln.References[0].URL)

	assert.Len(t, vuln.Affected, 1)
	assert.Equal(t, "test-package", vuln.Affected[0].Package.Name)
	assert.Equal(t, "npm", vuln.Affected[0].Package.Ecosystem)
}

```

--------------------------------------------------------------------------------
/pkg/osv/client.go:
--------------------------------------------------------------------------------

```go
// Package osv provides functionality for interacting with osv database.
package osv

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

const (
	// BaseURL is the base URL for the OSV API
	BaseURL = "https://api.osv.dev/v1"

	// QueryEndpoint is the endpoint for querying vulnerabilities
	QueryEndpoint = "/query"

	// QueryBatchEndpoint is the endpoint for batch querying vulnerabilities
	QueryBatchEndpoint = "/querybatch"

	// VulnEndpoint is the endpoint for getting vulnerability details
	VulnEndpoint = "/vulns"
)

// OSVClient is the interface for the OSV API client
//
//nolint:revive // OSVClient is intentionally named to provide clarity on the client's domain
type OSVClient interface {
	Query(ctx context.Context, req QueryRequest) (*QueryResponse, error)
	QueryBatch(ctx context.Context, req QueryBatchRequest) (*QueryBatchResponse, error)
	GetVulnerability(ctx context.Context, id string) (*Vulnerability, error)
}

// Client is a client for the OSV API
type Client struct {
	httpClient *http.Client
	baseURL    string
}

// NewClient creates a new OSV API client
func NewClient(opts ...ClientOption) *Client {
	client := &Client{
		httpClient: &http.Client{
			Timeout: 60 * time.Second, // Increased timeout to 60 seconds
		},
		baseURL: BaseURL,
	}

	for _, opt := range opts {
		opt(client)
	}

	return client
}

// ClientOption is a function that configures a Client
type ClientOption func(*Client)

// WithHTTPClient sets the HTTP client to use
func WithHTTPClient(httpClient *http.Client) ClientOption {
	return func(c *Client) {
		c.httpClient = httpClient
	}
}

// WithBaseURL sets the base URL to use
func WithBaseURL(baseURL string) ClientOption {
	return func(c *Client) {
		c.baseURL = baseURL
	}
}

// Package represents a package in the OSV API
type Package struct {
	Name      string `json:"name,omitempty"`
	Ecosystem string `json:"ecosystem,omitempty"`
	PURL      string `json:"purl,omitempty"`
}

// QueryRequest represents a request to the OSV API query endpoint
type QueryRequest struct {
	Commit    string  `json:"commit,omitempty"`
	Version   string  `json:"version,omitempty"`
	Package   Package `json:"package,omitempty"`
	PageToken string  `json:"page_token,omitempty"`
}

// QueryBatchRequest represents a request to the OSV API batch query endpoint
type QueryBatchRequest struct {
	Queries []QueryRequest `json:"queries"`
}

// Reference represents a reference in a vulnerability
type Reference struct {
	Type string `json:"type"`
	URL  string `json:"url"`
}

// Event represents an event in a vulnerability's timeline
type Event struct {
	Introduced string `json:"introduced,omitempty"`
	Fixed      string `json:"fixed,omitempty"`
	Limit      string `json:"limit,omitempty"`
}

// Range represents a range of versions affected by a vulnerability
type Range struct {
	Type   string  `json:"type"`
	Repo   string  `json:"repo,omitempty"`
	Events []Event `json:"events"`
}

// Affected represents a package affected by a vulnerability
type Affected struct {
	Package           Package                `json:"package"`
	Ranges            []Range                `json:"ranges,omitempty"`
	Versions          []string               `json:"versions,omitempty"`
	EcosystemSpecific map[string]interface{} `json:"ecosystem_specific,omitempty"`
	DatabaseSpecific  map[string]interface{} `json:"database_specific,omitempty"`
}

// Vulnerability represents a vulnerability in the OSV API
type Vulnerability struct {
	ID            string      `json:"id"`
	Summary       string      `json:"summary,omitempty"`
	Details       string      `json:"details,omitempty"`
	Modified      time.Time   `json:"modified"`
	Published     time.Time   `json:"published,omitempty"`
	References    []Reference `json:"references,omitempty"`
	Affected      []Affected  `json:"affected,omitempty"`
	SchemaVersion string      `json:"schema_version,omitempty"`
}

// QueryResponse represents a response from the OSV API query endpoint
type QueryResponse struct {
	Vulns         []Vulnerability `json:"vulns"`
	NextPageToken string          `json:"next_page_token,omitempty"`
}

// BatchQueryResult represents a single result in a batch query response
type BatchQueryResult struct {
	Vulns []struct {
		ID       string    `json:"id"`
		Modified time.Time `json:"modified"`
	} `json:"vulns"`
	NextPageToken string `json:"next_page_token,omitempty"`
}

// QueryBatchResponse represents a response from the OSV API batch query endpoint
type QueryBatchResponse struct {
	Results []BatchQueryResult `json:"results"`
}

// Query queries the OSV API for vulnerabilities matching the given request
func (c *Client) Query(ctx context.Context, req QueryRequest) (*QueryResponse, error) {
	_ = ctx
	url := fmt.Sprintf("%s%s", c.baseURL, QueryEndpoint)

	reqBody, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	// Create a new context with a 30-second timeout
	reqCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, url, bytes.NewReader(reqBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			// Log the error or handle it as appropriate for your application
			fmt.Printf("Error closing response body: %v\n", err)
		}
	}()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var queryResp QueryResponse
	if err := json.NewDecoder(resp.Body).Decode(&queryResp); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	return &queryResp, nil
}

// QueryBatch queries the OSV API for vulnerabilities matching the given batch request
func (c *Client) QueryBatch(ctx context.Context, req QueryBatchRequest) (*QueryBatchResponse, error) {
	_ = ctx
	url := fmt.Sprintf("%s%s", c.baseURL, QueryBatchEndpoint)

	reqBody, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	// Create a new context with a 30-second timeout
	reqCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, url, bytes.NewReader(reqBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			// Log the error or handle it as appropriate for your application
			fmt.Printf("Error closing response body: %v\n", err)
		}
	}()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var batchResp QueryBatchResponse
	if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	return &batchResp, nil
}

// GetVulnerability gets a vulnerability by ID
func (c *Client) GetVulnerability(ctx context.Context, id string) (*Vulnerability, error) {
	_ = ctx
	url := fmt.Sprintf("%s%s/%s", c.baseURL, VulnEndpoint, id)

	// Create a new context with a 30-second timeout
	reqCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			// Log the error or handle it as appropriate for your application
			fmt.Printf("Error closing response body: %v\n", err)
		}
	}()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var vuln Vulnerability
	if err := json.NewDecoder(resp.Body).Decode(&vuln); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	return &vuln, nil
}

```

--------------------------------------------------------------------------------
/pkg/mcp/server.go:
--------------------------------------------------------------------------------

```go
// Package mcp provides MCP server tools for OSV.
package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"

	"github.com/StacklokLabs/osv-mcp/pkg/osv"
)

const (
	// ServerName is the name of the MCP server
	ServerName = "osv-mcp"

	// ServerVersion is the version of the MCP server
	ServerVersion = "0.1.0"
	// ServerPort is the port the MCP server will listen on
	ServerPort = "8080"
)

// Server is an MCP server that provides OSV vulnerability information
type Server struct {
	mcpServer *server.MCPServer
	osvClient osv.OSVClient
}

// NewServer creates a new OSV MCP server
func NewServer(opts ...ServerOption) *Server {
	s := &Server{
		osvClient: osv.NewClient(),
	}

	for _, opt := range opts {
		opt(s)
	}

	mcpServer := server.NewMCPServer(ServerName, ServerVersion)

	// Register tools
	mcpServer.AddTool(
		mcp.NewToolWithRawSchema(
			"query_vulnerability",
			"Query for vulnerabilities affecting a specific package version or commit",
			json.RawMessage(`{
				"type": "object",
				"properties": {
					"commit": {
						"type": "string",
						"description": "The commit hash to query for. If specified, version should not be set."
					},
					"version": {
						"type": "string",
						"description": "The version string to query for. If specified, commit should not be set."
					},
					"package_name": {
						"type": "string",
						"description": "The name of the package."
					},
					"ecosystem": {
						"type": "string",
						"description": "The ecosystem for this package (e.g., PyPI, npm, Go)."
					},
					"purl": {
						"type": "string",
						"description": "The package URL for this package. If purl is used, package_name and ecosystem should not be set."
					}
				},
				"required": []
			}`),
		),
		s.handleQueryVulnerability,
	)

	mcpServer.AddTool(
		mcp.NewToolWithRawSchema(
			"query_vulnerabilities_batch",
			"Query for vulnerabilities affecting multiple packages or commits at once",
			json.RawMessage(`{
				"type": "object",
				"properties": {
					"queries": {
						"type": "array",
						"description": "Array of query objects",
						"items": {
							"type": "object",
							"properties": {
								"commit": {
									"type": "string",
									"description": "The commit hash to query for. If specified, version should not be set."
								},
								"version": {
									"type": "string",
									"description": "The version string to query for. If specified, commit should not be set."
								},
								"package_name": {
									"type": "string",
									"description": "The name of the package."
								},
								"ecosystem": {
									"type": "string",
									"description": "The ecosystem for this package (e.g., PyPI, npm, Go)."
								},
								"purl": {
									"type": "string",
									"description": "The package URL for this package. If purl is used, package_name and ecosystem should not be set."
								}
							}
						}
					}
				},
				"required": ["queries"]
			}`),
		),
		s.handleQueryVulnerabilitiesBatch,
	)

	mcpServer.AddTool(
		mcp.NewToolWithRawSchema(
			"get_vulnerability",
			"Get details for a specific vulnerability by ID",
			json.RawMessage(`{
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "The OSV vulnerability ID"
					}
				},
				"required": ["id"]
			}`),
		),
		s.handleGetVulnerability,
	)

	s.mcpServer = mcpServer
	return s
}

// ServerOption is a function that configures a Server
type ServerOption func(*Server)

// WithOSVClient sets the OSV client to use
func WithOSVClient(client osv.OSVClient) ServerOption {
	return func(s *Server) {
		s.osvClient = client
	}
}

// ServeSSE starts the MCP server using SSE
func (s *Server) ServeSSE(addr string) error {
	log.Printf("Starting OSV MCP server (SSE) on %s", addr)
	sseServer := server.NewSSEServer(s.mcpServer)
	return sseServer.Start(addr)
}

// ServeHTTPStream starts the MCP server using Streamable HTTP transport
func (s *Server) ServeHTTPStream(addr string) error {
	log.Printf("Starting OSV MCP server (Streamable HTTP) on %s", addr)

	httpSrv := server.NewStreamableHTTPServer(s.mcpServer,
		server.WithEndpointPath("/mcp/"),
		server.WithStateLess(true), // stateless mode
		server.WithHeartbeatInterval(30*time.Second),
	)

	return httpSrv.Start(addr)
}

// handleQueryVulnerability handles the query_vulnerability tool
func (s *Server) handleQueryVulnerability(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	commit := mcp.ParseString(request, "commit", "")
	version := mcp.ParseString(request, "version", "")
	packageName := mcp.ParseString(request, "package_name", "")
	ecosystem := mcp.ParseString(request, "ecosystem", "")
	purl := mcp.ParseString(request, "purl", "")

	// Validate input
	if commit != "" && version != "" {
		return mcp.NewToolResultError("Both commit and version cannot be specified"), nil
	}

	if purl != "" && (packageName != "" || ecosystem != "") {
		return mcp.NewToolResultError("If purl is specified, package_name and ecosystem should not be specified"), nil
	}

	if purl == "" && (packageName == "" || ecosystem == "") && commit == "" {
		return mcp.NewToolResultError("Either purl, or both package_name and ecosystem, or commit must be specified"), nil
	}

	// Create query request
	queryReq := osv.QueryRequest{
		Commit:  commit,
		Version: version,
	}

	if purl != "" {
		queryReq.Package = osv.Package{
			PURL: purl,
		}
	} else if packageName != "" && ecosystem != "" {
		queryReq.Package = osv.Package{
			Name:      packageName,
			Ecosystem: ecosystem,
		}
	}

	// Query OSV API
	resp, err := s.osvClient.Query(ctx, queryReq)
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to query OSV API", err), nil
	}

	// Format response
	result, err := json.MarshalIndent(resp, "", "  ")
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to marshal response", err), nil
	}

	return mcp.NewToolResultText(string(result)), nil
}

// handleQueryVulnerabilitiesBatch handles the query_vulnerabilities_batch tool
func (s *Server) handleQueryVulnerabilitiesBatch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args, ok := request.Params.Arguments.(map[string]interface{})
	if !ok {
		return mcp.NewToolResultError("Invalid arguments format"), nil
	}

	queriesRaw, ok := args["queries"].([]interface{})
	if !ok {
		return mcp.NewToolResultError("Invalid 'queries' parameter: must be array"), nil
	}

	// Convert queries to QueryRequest objects
	var queries []osv.QueryRequest
	for i, queryRaw := range queriesRaw {
		queryMap, ok := queryRaw.(map[string]interface{})
		if !ok {
			return mcp.NewToolResultError(fmt.Sprintf("Invalid query at index %d", i)), nil
		}

		commit, _ := queryMap["commit"].(string)
		version, _ := queryMap["version"].(string)
		packageName, _ := queryMap["package_name"].(string)
		ecosystem, _ := queryMap["ecosystem"].(string)
		purl, _ := queryMap["purl"].(string)

		// Validate input
		if commit != "" && version != "" {
			return mcp.NewToolResultError(fmt.Sprintf("Both commit and version cannot be specified in query %d", i)), nil
		}

		if purl != "" && (packageName != "" || ecosystem != "") {
			return mcp.NewToolResultError(
				fmt.Sprintf("If purl is specified, package_name and ecosystem should not be specified in query %d", i),
			), nil
		}

		// Create query request
		queryReq := osv.QueryRequest{
			Commit:  commit,
			Version: version,
		}

		if purl != "" {
			queryReq.Package = osv.Package{
				PURL: purl,
			}
		} else if packageName != "" && ecosystem != "" {
			queryReq.Package = osv.Package{
				Name:      packageName,
				Ecosystem: ecosystem,
			}
		}

		queries = append(queries, queryReq)
	}

	// Query OSV API
	resp, err := s.osvClient.QueryBatch(ctx, osv.QueryBatchRequest{Queries: queries})
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to query OSV API", err), nil
	}

	// Format response
	result, err := json.MarshalIndent(resp, "", "  ")
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to marshal response", err), nil
	}

	return mcp.NewToolResultText(string(result)), nil
}

// handleGetVulnerability handles the get_vulnerability tool
func (s *Server) handleGetVulnerability(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := mcp.ParseString(request, "id", "")
	if id == "" {
		return mcp.NewToolResultError("Vulnerability ID is required"), nil
	}

	// Get vulnerability from OSV API
	vuln, err := s.osvClient.GetVulnerability(ctx, id)
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to get vulnerability", err), nil
	}

	// Format response
	result, err := json.MarshalIndent(vuln, "", "  ")
	if err != nil {
		return mcp.NewToolResultErrorFromErr("Failed to marshal response", err), nil
	}

	return mcp.NewToolResultText(string(result)), nil
}

```

--------------------------------------------------------------------------------
/pkg/mcp/server_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/StacklokLabs/osv-mcp/pkg/osv"
)

// mockOSVClient is a mock implementation of the OSV client for testing
type mockOSVClient struct {
	queryFunc            func(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error)
	queryBatchFunc       func(ctx context.Context, req osv.QueryBatchRequest) (*osv.QueryBatchResponse, error)
	getVulnerabilityFunc func(ctx context.Context, id string) (*osv.Vulnerability, error)
}

func (m *mockOSVClient) Query(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error) {
	return m.queryFunc(ctx, req)
}

func (m *mockOSVClient) QueryBatch(ctx context.Context, req osv.QueryBatchRequest) (*osv.QueryBatchResponse, error) {
	return m.queryBatchFunc(ctx, req)
}

func (m *mockOSVClient) GetVulnerability(ctx context.Context, id string) (*osv.Vulnerability, error) {
	return m.getVulnerabilityFunc(ctx, id)
}

// newMockOSVClient creates a new mock OSV client with default implementations
func newMockOSVClient() *mockOSVClient {
	return &mockOSVClient{
		queryFunc: func(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error) {
			_, _ = ctx, req
			return &osv.QueryResponse{
				Vulns: []osv.Vulnerability{
					{
						ID:       "TEST-2023-001",
						Summary:  "Test vulnerability",
						Modified: time.Now(),
					},
				},
			}, nil
		},
		queryBatchFunc: func(ctx context.Context, req osv.QueryBatchRequest) (*osv.QueryBatchResponse, error) {
			_, _ = ctx, req
			return &osv.QueryBatchResponse{
				Results: []osv.BatchQueryResult{
					{
						Vulns: []struct {
							ID       string    `json:"id"`
							Modified time.Time `json:"modified"`
						}{
							{
								ID:       "TEST-2023-001",
								Modified: time.Now(),
							},
						},
					},
				},
			}, nil
		},
		getVulnerabilityFunc: func(ctx context.Context, id string) (*osv.Vulnerability, error) {
			_, _ = ctx, id
			return &osv.Vulnerability{
				ID:       "TEST-2023-001",
				Summary:  "Test vulnerability",
				Modified: time.Now(),
			}, nil
		},
	}
}

// getTextContent extracts the text content from a CallToolResult
func getTextContent(result *mcp.CallToolResult) string {
	if len(result.Content) == 0 {
		return ""
	}

	textContent, ok := mcp.AsTextContent(result.Content[0])
	if !ok {
		return ""
	}

	return textContent.Text
}

func TestHandleQueryVulnerability(t *testing.T) {
	// Create mock OSV client
	mockClient := newMockOSVClient()

	// Set up expected query parameters
	expectedPackageName := "test-package"
	expectedEcosystem := "npm"
	expectedVersion := "1.0.0"

	// Override query function to check parameters
	mockClient.queryFunc = func(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error) {
		_, _ = ctx, req
		assert.Equal(t, expectedPackageName, req.Package.Name)
		assert.Equal(t, expectedEcosystem, req.Package.Ecosystem)
		assert.Equal(t, expectedVersion, req.Version)

		return &osv.QueryResponse{
			Vulns: []osv.Vulnerability{
				{
					ID:       "TEST-2023-001",
					Summary:  "Test vulnerability",
					Modified: time.Now(),
				},
			},
		}, nil
	}

	// Create server with mock client
	server := NewServer(WithOSVClient(mockClient))

	// Create tool request
	request := mcp.CallToolRequest{}
	request.Params.Arguments = map[string]interface{}{
		"package_name": expectedPackageName,
		"ecosystem":    expectedEcosystem,
		"version":      expectedVersion,
	}

	// Call handler
	result, err := server.handleQueryVulnerability(context.Background(), request)

	// Check result
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.False(t, result.IsError)

	// Get text content
	text := getTextContent(result)
	assert.NotEmpty(t, text)

	// Parse result text as JSON
	var response osv.QueryResponse
	err = json.Unmarshal([]byte(text), &response)
	require.NoError(t, err)

	// Check response
	assert.Len(t, response.Vulns, 1)
	assert.Equal(t, "TEST-2023-001", response.Vulns[0].ID)
	assert.Equal(t, "Test vulnerability", response.Vulns[0].Summary)
}

func TestHandleQueryVulnerabilityWithPURL(t *testing.T) {
	// Create mock OSV client
	mockClient := newMockOSVClient()

	// Set up expected query parameters
	expectedPURL := "pkg:npm/[email protected]"

	// Override query function to check parameters
	mockClient.queryFunc = func(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error) {
		_, _ = ctx, req
		assert.Equal(t, expectedPURL, req.Package.PURL)
		assert.Empty(t, req.Package.Name)
		assert.Empty(t, req.Package.Ecosystem)

		return &osv.QueryResponse{
			Vulns: []osv.Vulnerability{
				{
					ID:       "TEST-2023-001",
					Summary:  "Test vulnerability",
					Modified: time.Now(),
				},
			},
		}, nil
	}

	// Create server with mock client
	server := NewServer(WithOSVClient(mockClient))

	// Create tool request
	request := mcp.CallToolRequest{}
	request.Params.Arguments = map[string]interface{}{
		"purl": expectedPURL,
	}

	// Call handler
	result, err := server.handleQueryVulnerability(context.Background(), request)

	// Check result
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.False(t, result.IsError)
	assert.NotEmpty(t, getTextContent(result))
}

func TestHandleQueryVulnerabilityWithCommit(t *testing.T) {
	// Create mock OSV client
	mockClient := newMockOSVClient()

	// Set up expected query parameters
	expectedCommit := "abcdef1234567890"

	// Override query function to check parameters
	mockClient.queryFunc = func(ctx context.Context, req osv.QueryRequest) (*osv.QueryResponse, error) {
		_, _ = ctx, req
		assert.Equal(t, expectedCommit, req.Commit)
		assert.Empty(t, req.Version)

		return &osv.QueryResponse{
			Vulns: []osv.Vulnerability{
				{
					ID:       "TEST-2023-001",
					Summary:  "Test vulnerability",
					Modified: time.Now(),
				},
			},
		}, nil
	}

	// Create server with mock client
	server := NewServer(WithOSVClient(mockClient))

	// Create tool request
	request := mcp.CallToolRequest{}
	request.Params.Arguments = map[string]interface{}{
		"commit": expectedCommit,
	}

	// Call handler
	result, err := server.handleQueryVulnerability(context.Background(), request)

	// Check result
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.False(t, result.IsError)
	assert.NotEmpty(t, getTextContent(result))
}

func TestHandleQueryVulnerabilitiesBatch(t *testing.T) {
	// Create mock OSV client
	mockClient := newMockOSVClient()

	// Override query batch function to check parameters
	mockClient.queryBatchFunc = func(ctx context.Context, req osv.QueryBatchRequest) (*osv.QueryBatchResponse, error) {
		_, _ = ctx, req
		assert.Len(t, req.Queries, 2)
		assert.Equal(t, "test-package-1", req.Queries[0].Package.Name)
		assert.Equal(t, "npm", req.Queries[0].Package.Ecosystem)
		assert.Equal(t, "1.0.0", req.Queries[0].Version)
		assert.Equal(t, "test-package-2", req.Queries[1].Package.Name)
		assert.Equal(t, "npm", req.Queries[1].Package.Ecosystem)
		assert.Equal(t, "2.0.0", req.Queries[1].Version)

		return &osv.QueryBatchResponse{
			Results: []osv.BatchQueryResult{
				{
					Vulns: []struct {
						ID       string    `json:"id"`
						Modified time.Time `json:"modified"`
					}{
						{
							ID:       "TEST-2023-001",
							Modified: time.Now(),
						},
					},
				},
				{
					Vulns: []struct {
						ID       string    `json:"id"`
						Modified time.Time `json:"modified"`
					}{
						{
							ID:       "TEST-2023-002",
							Modified: time.Now(),
						},
					},
				},
			},
		}, nil
	}

	// Create server with mock client
	server := NewServer(WithOSVClient(mockClient))

	// Create tool request
	request := mcp.CallToolRequest{}
	request.Params.Arguments = map[string]interface{}{
		"queries": []interface{}{
			map[string]interface{}{
				"package_name": "test-package-1",
				"ecosystem":    "npm",
				"version":      "1.0.0",
			},
			map[string]interface{}{
				"package_name": "test-package-2",
				"ecosystem":    "npm",
				"version":      "2.0.0",
			},
		},
	}

	// Call handler
	result, err := server.handleQueryVulnerabilitiesBatch(context.Background(), request)

	// Check result
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.False(t, result.IsError)

	// Get text content
	text := getTextContent(result)
	assert.NotEmpty(t, text)

	// Parse result text as JSON
	var response osv.QueryBatchResponse
	err = json.Unmarshal([]byte(text), &response)
	require.NoError(t, err)

	// Check response
	assert.Len(t, response.Results, 2)
	assert.Len(t, response.Results[0].Vulns, 1)
	assert.Equal(t, "TEST-2023-001", response.Results[0].Vulns[0].ID)
	assert.Len(t, response.Results[1].Vulns, 1)
	assert.Equal(t, "TEST-2023-002", response.Results[1].Vulns[0].ID)
}

func TestHandleGetVulnerability(t *testing.T) {
	// Create mock OSV client
	mockClient := newMockOSVClient()

	// Set up expected query parameters
	expectedID := "TEST-2023-001"

	// Override get vulnerability function to check parameters
	mockClient.getVulnerabilityFunc = func(ctx context.Context, id string) (*osv.Vulnerability, error) {
		_ = ctx
		assert.Equal(t, expectedID, id)

		return &osv.Vulnerability{
			ID:       expectedID,
			Summary:  "Test vulnerability",
			Details:  "This is a test vulnerability",
			Modified: time.Now(),
		}, nil
	}

	// Create server with mock client
	server := NewServer(WithOSVClient(mockClient))

	// Create tool request
	request := mcp.CallToolRequest{}
	request.Params.Arguments = map[string]interface{}{
		"id": expectedID,
	}

	// Call handler
	result, err := server.handleGetVulnerability(context.Background(), request)

	// Check result
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.False(t, result.IsError)

	// Get text content
	text := getTextContent(result)
	assert.NotEmpty(t, text)

	// Parse result text as JSON
	var vuln osv.Vulnerability
	err = json.Unmarshal([]byte(text), &vuln)
	require.NoError(t, err)

	// Check response
	assert.Equal(t, expectedID, vuln.ID)
	assert.Equal(t, "Test vulnerability", vuln.Summary)
	assert.Equal(t, "This is a test vulnerability", vuln.Details)
}

```