Skip to main content

Quality and security platform for AI agents. Validate MCP servers, scan for vulnerabilities, ship reliable agents.

Project description

agent-lint

ESLint for agents.

Validate MCP servers, scan for vulnerabilities, write integration tests, and ship reliable agents.

PyPI Python License: MIT


Install

pip install agent-lint-cli

Validate an MCP Server

agent-lint validate https://my-mcp-server.com
๐Ÿ” Validating MCP Server: https://my-mcp-server.com

Schema Validation
  โœ“ Valid JSON-RPC 2.0 endpoint
  โœ“ Tools list returned (5 tools)
  โœ“ Resources endpoint responds

Security Analysis
  โš  [MEDIUM] Tool "run_query" has parameter "sql" without type validation
  โš  [MEDIUM] Tool "execute_task" has risky name pattern
  โœ“ No credential patterns detected
  โœ“ Rate limiting headers present

Tool Quality
  โœ“ All tools have descriptions
  โœ— Tool "helper" missing parameter descriptions

Performance
  โœ“ Response time: 245ms
  โš  Large response payload: 2.3MB

โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
Security Score: 75/100
Quality Score:  68/100

Options

Flag Description
--format console|json|sarif Output format (default: console)
--fail-under N Exit 1 if overall score is below N (0โ€“100)
--fail-on-security Exit 1 if any security issues are found
--security-level strict|standard|permissive|none Check strictness (default: standard)
--dynamic Run dynamic security tests (calls tools with payloads)
--policy PATH Path to .agent-lint.yaml policy file
--junit-xml PATH Write JUnit XML results to file

CI/CD Integration

GitHub Actions

# .github/workflows/mcp-security.yml
name: MCP Security

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/agent-lint
        with:
          url: https://my-mcp-server.com
          fail-on-security: "true"
          security-level: strict

The action automatically uploads SARIF results to the GitHub Security tab.

Inline with CLI

# Fail on any security issue
agent-lint validate https://my-server.com --fail-on-security

# Require minimum score
agent-lint validate https://my-server.com --fail-under 80

# SARIF output for GitHub Security tab
agent-lint validate https://my-server.com -f sarif > results.sarif

# JUnit XML for test reporting
agent-lint validate https://my-server.com --junit-xml results.xml

# Dynamic security testing (calls tools with injection payloads)
agent-lint validate https://my-server.com --dynamic

Security Policies

Create an .agent-lint.yaml file to enforce rules across your team:

# .agent-lint.yaml
security:
  level: strict

  rules:
    dangerous_patterns: error
    missing_validation: error
    overly_permissive: warn
    credential_exposure: error

  exceptions:
    - tool: admin_execute
      reason: "Admin-only endpoint with auth middleware"
      rules:
        - dangerous_patterns

Then run with:

agent-lint validate https://my-server.com --policy .agent-lint.yaml

Policy security level overrides --security-level unless you pass --security-level explicitly.


MCP Testing (pytest)

agent-lint ships a pytest plugin and fixtures for writing integration tests against MCP servers.

Install testing extras

pip install "agent-lint-cli[testing]"

Write tests

# tests/test_my_server.py
from agent_lint import mcp


class TestQueryTool:

    async def test_basic_query(self, mcp_client):
        response = await mcp_client.call_tool("query_database", {"sql": "SELECT 1"})
        mcp.assert_valid_response(response)

    async def test_rejects_sql_injection(self, mcp_client):
        response = await mcp_client.call_tool(
            "query_database",
            {"sql": "'; DROP TABLE users; --"},
        )
        mcp.assert_error(response)

    async def test_response_time(self, mcp_client):
        response = await mcp_client.call_tool("query_database", {"sql": "SELECT 1"})
        mcp.assert_response_time(response, max_ms=500)

    async def test_no_secrets_in_response(self, mcp_client):
        response = await mcp_client.call_tool("get_config", {})
        mcp.assert_no_secrets(response)

Run against a real server

# Against a live server
agent-lint test tests/ --mcp-url https://my-mcp-server.com

# Or with pytest directly
pytest tests/ --mcp-url https://my-mcp-server.com

Run with the mock server (no real server needed)

# tests/test_mock.py
async def test_with_mock(mock_mcp_server, mcp_client):
    mock_mcp_server.add_tool(
        "get_weather",
        description="Get weather for a city",
        input_schema={
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
        handler=lambda args: {"content": [{"type": "text", "text": f"Sunny in {args['city']}"}]},
    )

    response = await mcp_client.call_tool("get_weather", {"city": "London"})
    mcp.assert_valid_response(response)
    mcp.assert_content_matches(response, r"Sunny")

When --mcp-url is not passed, mcp_client automatically uses the mock server.


Assertion Reference

Assertion Description
mcp.assert_valid_response(r) Response has a result and no error
mcp.assert_error(r, code=None) Response is an error (optionally assert error code)
mcp.assert_result_contains(r, key, value) Result dict contains key (optionally assert value)
mcp.assert_content_text(r, expected) Response has a text content block matching string
mcp.assert_content_matches(r, pattern) Any text content block matches regex
mcp.assert_tool_exists(r, name) tools/list response contains a named tool
mcp.assert_no_secrets(r) Response does not contain credential patterns
mcp.assert_response_time(r, max_ms) Response was returned within the time limit

Security Checks

agent-lint runs these checks by default (static analysis on tool definitions):

Check Severity Description
Dangerous tool names Medium Names like execute, shell, eval, run_command
Risky parameters without validation Medium sql, cmd, path, url without JSON schema type
Overly permissive descriptions Medium Descriptions claiming "any file", "any command"
Credential exposure High API keys, tokens, secrets in responses
Missing rate limiting Low No rate-limit headers on responses

With --dynamic, agent-lint also calls tools with injection payloads to detect runtime vulnerabilities:

Payload Class Example Parameters Targeted
SQL injection sql, query, filter
Path traversal path, file, filename
Command injection cmd, command, exec, shell
XSS html, content, body
SSRF url, uri, endpoint

Note: Static analysis is a first line of defense on tool definitions only. It cannot analyze handler code or detect all runtime vulnerabilities. Use --dynamic for deeper testing.


Validate an A2A Agent

agent-lint validate https://my-agent.com --protocol a2a
๐Ÿ” Validating A2A Agent: https://my-agent.com

Schema Validation
  โœ“ Agent card fetched (/.well-known/agent.json)
  โœ“ Required fields present (name, url, version, skills)
  โœ“ All skills have IDs

Security Analysis
  โš  [HIGH] Agent served over HTTP โ€” use HTTPS in production
  โœ“ Authentication schemes defined
  โœ“ No injection patterns in skill descriptions

Quality
  โœ“ Agent description present
  โœ“ All skills have descriptions and tags

Performance
  โœ“ Response time: 87ms
  โœ“ Payload size: 1.2KB

โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
Security Score: 80/100
Quality Score:  100/100

A2A Security Checks

Check Severity Description
HTTPS enforcement High Agent URL must use HTTPS
Missing authentication High No authentication.schemes defined
Overly broad skills Medium Descriptions claiming "any file", "any command"
Prompt injection risk Critical Injection patterns in skill descriptions
Missing input schema Low Skills with no inputModes/outputModes
Missing version Low No version or placeholder version (0.0.0)

A2A Testing (pytest)

agent-lint includes a pytest plugin and fixtures for writing integration tests against A2A agents.

Write tests

# tests/test_my_agent.py
from agent_lint import a2a


class TestMyAgent:

    async def test_card_is_valid(self, a2a_client):
        card = await a2a_client.get_card()
        a2a.assert_valid_card(card)
        a2a.assert_skill_exists(card, "weather")

    async def test_skill_completes(self, a2a_client):
        response = await a2a_client.send_message("What is the weather?", skill_id="weather")
        a2a.assert_task_completed(response)
        a2a.assert_artifact_matches(response, r"sunny|cloudy|rain")

    async def test_no_secrets_in_response(self, a2a_client):
        response = await a2a_client.send_message("Hello", skill_id="weather")
        a2a.assert_no_secrets(response)

Run against a real agent

# Against a live agent
agent-lint test tests/ --a2a-url https://my-agent.com

# Or with pytest directly
pytest tests/ --a2a-url https://my-agent.com

Run with the mock agent (no real agent needed)

# tests/test_mock_agent.py
from agent_lint import a2a


async def test_with_mock(mock_a2a_agent, a2a_client):
    mock_a2a_agent.set_card(name="Weather Agent", description="Provides weather forecasts")
    mock_a2a_agent.add_skill(
        "weather",
        description="Get current weather for a location",
        tags=["weather", "forecast"],
        handler=lambda text, history: {"type": "text", "text": "Sunny, 72ยฐF"},
    )

    card = await a2a_client.get_card()
    a2a.assert_valid_card(card)

    response = await a2a_client.send_message("London weather", skill_id="weather")
    a2a.assert_task_completed(response)
    a2a.assert_artifact_matches(response, r"Sunny")

When --a2a-url is not passed, a2a_client automatically uses the mock agent.

Test MCP and A2A together

Both mcp_client and a2a_client fixtures are always available. Use them in the same test to validate workflows that span both protocols:

async def test_combined_workflow(mcp_client, a2a_client):
    # Fetch data via MCP tool
    data_response = await mcp_client.call_tool("get_data", {"id": "123"})
    mcp.assert_valid_response(data_response)

    # Send it to an A2A agent for analysis
    task = await a2a_client.send_message(str(data_response.result), skill_id="analyze")
    a2a.assert_task_completed(task)

A2A Assertion Reference

Assertion Description
a2a.assert_valid_card(r) Card was fetched and has required fields
a2a.assert_skill_exists(r, skill_id) Agent card contains the named skill
a2a.assert_task_completed(r) Task reached the completed state
a2a.assert_task_failed(r) Task reached the failed state
a2a.assert_task_state(r, state) Task is in the given state
a2a.assert_artifact_contains(r, text) Any artifact text part contains exact text
a2a.assert_artifact_matches(r, pattern) Any artifact text part matches regex
a2a.assert_no_secrets(r) Response does not contain credential patterns
a2a.assert_response_time(r, max_ms) Response was returned within the time limit

Roadmap

Version Status Features
v0.1 Released MCP validation + basic security
v0.2 Released MCP testing, pytest plugin, policy engine, CI/CD
v0.3 Current A2A validation and testing
v1.0 Planned Observability, trace capture and replay

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_lint_cli-0.3.0.tar.gz (55.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_lint_cli-0.3.0-py3-none-any.whl (46.9 kB view details)

Uploaded Python 3

File details

Details for the file agent_lint_cli-0.3.0.tar.gz.

File metadata

  • Download URL: agent_lint_cli-0.3.0.tar.gz
  • Upload date:
  • Size: 55.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for agent_lint_cli-0.3.0.tar.gz
Algorithm Hash digest
SHA256 da4cce2b4569640b0fdae6208063389ce7dc35cc0e1bd1b7c79db160fc036333
MD5 0c49e99893d60ce517ef7886a32145a2
BLAKE2b-256 8847c9774b81a805c67fa9319288fbd120281a4b238caee33aaae2105343fd9a

See more details on using hashes here.

File details

Details for the file agent_lint_cli-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: agent_lint_cli-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 46.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for agent_lint_cli-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd423223922ff3752b27d41a5257fd8880477a42e3a46b418a876994d9d6523b
MD5 6e251b22aa2228dd67328b5efff78df1
BLAKE2b-256 99225b7af09637bf0cfbcc74df6e655fa9e55574770522a47e1d727b7e5c42b4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page