Skip to main content

Pter MCP Tool Reference

This document provides a comprehensive reference for the Pter MCP tools available to agents.

Quick Reference Table

ToolActionsPurpose
manage_servicescreate, update, get, list, add_technologyService registry management
manage_endpointscreate, update, get, listAPI endpoint tracking
manage_flowscreate_flow, update_flow, get_flow, list_flows, delete_flowUser journey flows
manage_auth_sessionlist_sessions, get_current_session, replace_current_session, create_new_session, reauth, set_metadata, get_metadata, add_api_key, list_api_keys, update_capabilities, update_descriptionAuthentication session management
manage_assessmentscreate, update, get, list, delete, submit_resultSecurity hypothesis tracking
manage_findingscreate, update, get, list, delete, statisticsValidated security findings
manage_credentialscreate, list, get, update, delete, list_by_account, update_statusDiscovered credentials
manage_taskscreate, get_details, update_statusTask lifecycle management
save_memoryN/A (single action)Save knowledge to RAG
query_memoriesN/A (single action)Query saved memories

manage_services

Service registry management for tracking discovered services.

Actions

create

manage_services(
action="create",

name="auth-service",
base_url="https://api.target.com/auth/",
description="OAuth2/OIDC authentication service handling login, token issuance, and session management. Exposes /authorize, /token, and /userinfo endpoints. Uses RS256 JWT with 1-hour expiry."
)

get

service = manage_services(action="get", service_id=1)

update

# IMPORTANT: Always GET the entity first and read its current description
# before updating. Never blindly overwrite descriptions.
service = manage_services(action="get", service_id=1)
# Only update description if you are genuinely improving it (adding detail),
# not replacing it with phase notes.
manage_services(
action="update",

service_id=1,
description=service["description"] + ". Also serves legacy /api/v1/login endpoint (Basic auth, no MFA)."
)

list

all_services = manage_services(action="list")
# Filter in memory if needed:
matching = [s for s in all_services.get("services", []) if "auth" in s.get("name", "")]

add_technology

manage_services(
action="add_technology",

service_id=1,
tech_name="nginx", # Required
tech_category="web_server", # Required: web_server, framework, database, language, library, etc.
tech_version="1.24.0", # Optional
tech_confidence="high", # Optional: low, medium, high
tech_evidence="Server header: nginx/1.24.0" # Optional
)

Note: Assessments

Use manage_assessments(action="create", ...) instead of manage_services to create assessments targeting a service. Pass targets=[EntityID.of("service", service_id)].


manage_endpoints

Track discovered API endpoints and their security status.

Actions

create

manage_endpoints(
action="create",

url="https://api.target.com/users/search",
method="POST",
description="Full-text user search returning paginated results with email, display name, role, and last login. Requires authenticated session (any role). Rate-limited to 10 req/min.",
service_id=1, # Optional — link to parent service
openapi_schema={
"requestBody": {"content": {"application/json": {"schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"filters": {"type": "object", "properties": {"role": {"type": "string"}, "status": {"type": "string"}}},
"page": {"type": "integer", "default": 1}
},
"required": ["query"]
}}}},
"responses": {"200": {"description": "Paginated user list"}}
}
)

Use openapi_schema for request/response parameters, types, and schema details. Keep description focused on what the endpoint does and its security-relevant behavior (auth requirements, rate limits, data sensitivity).

get

endpoint = manage_endpoints(action="get", endpoint_id=1)

update

# IMPORTANT: Always GET the entity first and read its current description
# before updating. Never blindly overwrite descriptions.
endpoint = manage_endpoints(action="get", endpoint_id=1)
manage_endpoints(
action="update",

endpoint_id=1,
description=endpoint["description"] + ". Also accepts wildcard queries — no input length limit enforced server-side."
)

list

all_endpoints = manage_endpoints(action="list")

Response fields include: id, method, url, description, service_id, p4_task_id (linked P4 task), p4_task_status (status of linked P4 task).


manage_flows

Track user journeys and attack paths.

Actions

create_flow

manage_flows(
action="create_flow",

name="user_registration",
start_state="anonymous", # Required: initial state
end_state="registered", # Required: final state
description="Self-service registration: user submits email+password, receives verification email with signed token, clicks link to activate account. Creates user record with 'member' role. No admin approval required. Email uniqueness enforced at DB level.",
criticality="high",
required_privilege="none", # Optional
steps=[ # Optional: list of step dicts
{"order": 1, "url": "https://api.target.com/register", "method": "POST", "description": "Submit registration form"},
{"order": 2, "url": "https://api.target.com/verify", "method": "GET", "description": "Email verification"}
]
)

get_flow

flow = manage_flows(action="get_flow", flow_id=1)

update_flow

# IMPORTANT: Always GET the flow first and read its current description
# before updating. Never blindly overwrite descriptions.
flow = manage_flows(action="get_flow", flow_id=1)
manage_flows(
action="update_flow",

flow_id=1,
description=flow["description"] + ". Verification token has 24h expiry, no rate limit on resend.",
criticality="critical"
)

list_flows

all_flows = manage_flows(action="list_flows")

delete_flow

manage_flows(action="delete_flow", flow_id=1)

manage_auth_session

Manage authentication sessions, API keys, and metadata for cross-account testing.

Actions

list_sessions

sessions = manage_auth_session(action="list_sessions")

get_current_session

session = manage_auth_session(action="get_current_session", session_id="...")

replace_current_session

# IMPORTANT: Close the browser FIRST before switching sessions
manage_auth_session(action="replace_current_session", session_id="...")

create_new_session

manage_auth_session(
action="create_new_session",

login_url="https://target.com/login",
username="newuser@test.com",
password="password123",
display_name="Test Account",
account_role="user",
notes="Created during testing"
)

reauth

# Re-authenticate an expired or failing session
manage_auth_session(action="reauth", session_id="...")
# Spawns an auth agent to re-login. Check session status after a few moments.

set_metadata

manage_auth_session(
action="set_metadata",

session_id="...",
metadata_key="user_id",
metadata_value="12345"
)

get_metadata

value = manage_auth_session(action="get_metadata", session_id="...", metadata_key="user_id")

add_api_key

manage_auth_session(
action="add_api_key",

session_id="...",
key_name="Primary API Key",
api_key="sk-live-abc123..."
)

list_api_keys

keys = manage_auth_session(action="list_api_keys", session_id="...")

update_capabilities

manage_auth_session(
action="update_capabilities",

session_id="...",
capabilities={"can_create_users": True, "can_delete": False}
)

update_description

manage_auth_session(
action="update_description",

session_id="...",
description="Admin session with full access",
scope="admin"
)

manage_assessments

Track security hypotheses and tests. Three types by origin:

  • vector — agent-identified attack vector (e.g., SQL injection on an endpoint)
  • cve — known CVE matched to detected technology
  • chain — combining multiple findings for higher impact

Actions

create

manage_assessments(
action="create",

title="SQL Injection on /api/users/search",
description="POST parameter 'query' appears to be concatenated into SQL",
assessment_type="vector", # vector, cve, or chain
targets=["endpoint://42"], # Required, non-empty. Format: entity_type://id
status="pending" # pending → in_progress → confirmed / refuted
)

update

manage_assessments(
action="update",

assessment_id=42,
status="confirmed",
description="Confirmed via time-based blind injection"
)

submit_result

manage_assessments(
action="submit_result",

assessment_id=42, # Required
status="confirmed", # Required: "confirmed" or "refuted"
description="## Investigation Result\n\n**Verdict: Exploitable**\n\n...", # Required
report_path="work/docs/exploitation/exploitation_42_CWE-89.md" # Required
)

get / list

assessment = manage_assessments(action="get", assessment_id=42)
all_assessments = manage_assessments(action="list")

manage_findings

Submit and track validated security findings. Findings are the primary deliverable of the engagement — they go directly into client reports. Write them like a senior pentester would: precise, evidence-backed, and actionable.

Writing High-Quality Findings

title

One line. Name the vulnerability and where it lives. A reader should know what's broken without opening the finding.

  • Good: Stored XSS via unsanitized markdown in /api/comments
  • Good: Privilege escalation from viewer to admin via IDOR on /api/users/{id}/role
  • Bad: XSS vulnerability (where? what kind?)
  • Bad: Security issue in API (meaningless)

description

The technical narrative. 2-4 paragraphs covering:

  1. What — the vulnerability class and root cause (not just the symptom)
  2. Where — exact endpoint, parameter, function, or component
  3. Why it matters — concrete impact in context of this application (data exposure, account takeover, lateral movement — not generic risk language)
  4. Conditions — authentication requirements, privileges needed, rate limits, or other constraints
The /api/v2/documents/{id} endpoint does not validate that the authenticated
user has access to the requested document. Any authenticated user can retrieve
any document by iterating over document IDs, including documents belonging to
other organizations.

This is a horizontal privilege escalation — a user in Org A can access
confidential documents uploaded by Org B. The endpoint returns the full
document body including file attachments, making this a direct data breach
vector rather than a metadata leak.

The vulnerability requires only a valid session token (any role). No
additional privileges are needed. Document IDs are sequential integers,
making enumeration trivial.

Don't: Write one-liners. Repeat the title. Use filler like "this could potentially allow an attacker to possibly..." — state what happens.

cvss_vector

Required. Every finding must have a CVSS 3.1 vector. The severity field is computed from it — never set severity directly.

Build the vector using the CVSS 3.1 specification:

  • Attack Vector (AV), Attack Complexity (AC), Privileges Required (PR), User Interaction (UI)
  • Scope (S), Confidentiality (C), Integrity (I), Availability (A)

The score maps to severity automatically:

ScoreSeverity
9.0 – 10.0critical
7.0 – 8.9high
4.0 – 6.9medium
0.1 – 3.9low
0.0informational
# Unauthenticated IDOR leaking sensitive data
# AV:N (network) AC:L (no special conditions) PR:N (no auth) UI:N (no interaction)
# S:U (no scope change) C:H (full document contents) I:N (read-only) A:N
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" # → 7.5 → high

Don't: File a finding without a cvss_vector. If you can't score it, you don't understand the impact well enough to file it.

reproduction_steps

Step-by-step instructions a human pentester can follow to reproduce the finding independently. Each step must include either a full shell command (curl, sqlmap, nuclei, etc.) or manual browser/UI steps with exact clicks and inputs. Every step must show the expected output that confirms the vulnerability.

Use $VARIABLE shell syntax for placeholders that need to be filled in (e.g., $TOKEN, $SESSION_ID, $TARGET_URL). Never use <VARIABLE> — it breaks copy-paste into a shell and looks like HTML.

1. Authenticate as test@org-a.com (viewer role):
$ curl -s -X POST $TARGET_URL/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email": "test@org-a.com", "password": "TestPass123!"}'

Output: {"access_token": "eyJhbG...","user": {"org_id": "org-a", "role": "viewer"}}

2. Request a document belonging to a different organization:
$ curl -s -H "Authorization: Bearer $TOKEN" \
$TARGET_URL/api/v2/documents/847

Output: {"id": 847, "org_id": "org-b", "title": "Q3 Financial Report", "body": "..."}
Expected: 403 Forbidden. Actual: 200 with full document body from org-b.

3. Enumerate accessible documents across organizations:
$ for id in $(seq 1 100); do \
curl -s -o /dev/null -w "%{http_code} $id\n" \
-H "Authorization: Bearer $TOKEN" \
$TARGET_URL/api/v2/documents/$id; \
done | grep "^200"

Output:
200 12
200 47
200 103
200 847
... (~40 documents from other organizations returned 200)

For browser-based reproduction, specify exact navigation and interactions:

1. Log in to https://target.com/login as test@org-a.com / TestPass123!
→ Lands on /dashboard showing "Org A" in the top-right corner

2. Open browser DevTools → Network tab

3. Navigate to Documents → click any document → note the URL: /documents/847
→ URL contains a numeric document ID

4. In the browser address bar, change the ID: /documents/1
→ Page loads a document titled "Org B Internal Policy"
→ DevTools shows GET /api/v2/documents/1 returned 200 with org_id: "org-b"
Expected: error page or redirect. Actual: full document rendered.

Don't: Write "send a malicious request" without showing the request. Omit response output. Leave the reader guessing what "success" looks like — always contrast expected vs. actual behavior.

evidence

Proof that the vulnerability exists and was exploited, not just suspected. Each item should have type, the command/request, and the response.

evidence=[
{
"type": "request",
"command": "curl -H 'Authorization: Bearer eyJ...' https://target.com/api/v2/documents/847",
"response": '{"id": 847, "org_id": "org-b", "title": "Q3 Financial Report", "body": "..."}'
},
{
"type": "screenshot",
"path": "work/evidence/idor-document-847.png",
"description": "Document 847 belonging to org-b accessed by org-a user"
}
]

Include enough evidence to prove the finding without re-testing. Redact actual secrets but keep the structure.

Specific to this codebase. Reference the pattern that should change.

  • Good: Add authorization check in document_router.get_document() to verify request.user.org_id matches document.org_id before returning. Use the existing OrgAccessGuard middleware.
  • Bad: Implement proper access controls (says nothing actionable)

Common mistakes to avoid

  • Generic descriptions copied from CWE/OWASP — always ground findings in this specific application
  • Missing context — don't assume the reader tested the app; explain the feature being tested
  • Conflating findings — one vulnerability per finding; don't bundle "XSS and also CSRF" together
  • Skipping evidence — a finding without evidence is a claim, not a finding
  • No CVSS vector — every finding needs a calculated vector, not a gut-feel severity

Actions

create

manage_findings(
action="create",

# --- REQUIRED fields ---
title="Time-based blind SQL injection in /api/users search",
description=(
"The /api/users/search endpoint is vulnerable to time-based blind SQL injection "
"via the 'query' parameter. The parameter value is interpolated directly into a "
"SQL WHERE clause without parameterization.\n\n"
"An attacker can extract arbitrary data from the database by observing response "
"timing differences. The users table contains email addresses, password hashes, "
"and API keys for all tenants — this is a full database compromise vector.\n\n"
"No authentication is required. The search endpoint is public-facing and has no "
"rate limiting."
),
affected_components=["endpoint://42"], # Required, non-empty. Format: entity_type://id
report_path="work/docs/exploitation/exploitation_42.md", # Required: path to report file
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", # Required: severity is derived from this

# --- optional fields ---
cwe_id="CWE-89",
reproduction_steps=(
"1. Send a baseline search request:\n"
" $ curl -s -w '\\nTime: %{time_total}s\\n' -X POST https://target.com/api/users/search \\\n"
" -H 'Content-Type: application/json' \\\n"
" -d '{\"query\": \"admin\"}'\n\n"
" Output: {\"users\": [{\"id\": 1, ...}]}\n"
" Time: 0.12s\n\n"
"2. Send a time-based injection payload:\n"
" $ curl -s -w '\\nTime: %{time_total}s\\n' -X POST https://target.com/api/users/search \\\n"
" -H 'Content-Type: application/json' \\\n"
" -d '{\"query\": \"admin\\' AND SLEEP(5)-- -\"}'\n\n"
" Output: {\"users\": []}\n"
" Time: 5.14s\n"
" Expected: <0.5s response or input rejection. Actual: 5s delay confirms injection."
),
recommended_fix="Use parameterized queries in user_repository.search(). Replace f-string SQL with sqlalchemy text() bind parameters.",
evidence=[
{
"type": "request",
"command": "curl -s -w '\\nTime: %{time_total}s\\n' -X POST https://target.com/api/users/search -H 'Content-Type: application/json' -d '{\"query\": \"admin\\' AND SLEEP(5)-- -\"}'",
"response": "{\"users\": []}\nTime: 5.14s"
}
],
assessment_id=42, # Link to the assessment that confirmed this
)

update

manage_findings(
action="update",

finding_id=1,
status="validated", # draft → validated → submitted → accepted → remediated → remediation_verified
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", # Recalculated after further testing
)

get / list / statistics

finding = manage_findings(action="get", finding_id=1)
all_findings = manage_findings(action="list")
stats = manage_findings(action="statistics")

manage_credentials

Store discovered credentials (passwords, JWTs, API keys, session cookies). Optionally linked to an Account.

IMPORTANT: Use this tool for credential values, NOT save_memory. Credentials have their own entity with type, status tracking, and account linkage.

Actions

create

manage_credentials(
action="create",

credential_type="api_key", # user_password, jwt, api_key, session_cookie, other
value="sk-live-abc123...",
account_id=5, # Optional — link to discovered Account
status="valid", # unknown, valid, invalid, expired
notes="Found in /config/settings.json"
)

list / get / update / delete

all_creds = manage_credentials(action="list")
cred = manage_credentials(action="get", credential_id=1)
manage_credentials(action="update_status", credential_id=1, status="expired")
by_account = manage_credentials(action="list_by_account", account_id=5)

manage_tasks

Task lifecycle management.

Actions

create

# General task creation — title and phase_id are REQUIRED
manage_tasks(
action="create",

title="Investigate SQLi on /api/users", # Required: short label
task_description="Investigate SQL injection on /api/users",
done_definition="Vulnerability confirmed or rejected with evidence",
phase_id=5, # Required
priority="high"
)

# P4 task with endpoint linking (REQUIRED for P4 tasks)
endpoint = manage_endpoints(action="create", url=..., method=..., ...)
manage_tasks(
action="create",

title=f"P4: {endpoint['method']} {endpoint['url']}", # Required
task_description=f"Phase 4: Reconnaissance for {endpoint['method']} {endpoint['url']}",
done_definition="Research CWEs and create investigation tasks",
phase_id=4, # Required
priority="high",
endpoint_id=endpoint["id"], # MUST pass for P4 tasks
)

# P2 task with service linking (optional for P2 tasks)
manage_tasks(
action="create",
title=f"P2: Explore {service_name}", # Required
task_description=f"Phase 2: Service exploration for {service_name}",
done_definition="Identify endpoints, technologies, and attack surface",
phase_id=2, # Required
service_id=service_id, # Links this task to the service (auto-populates service_ids)
)

get_details

task = manage_tasks(action="get_details", task_id=1)

update_status

manage_tasks(
action="update_status",

task_id=TASK_ID,
status="done",
summary="Confirmed SQL injection with time-based technique",
key_learnings=["Parameterized queries not used", "Error messages expose DB type"]
)

save_memory

Save observations and learnings to RAG for persistence and sharing with other agents.

Use save_memory for: observations, learnings, error fixes, decisions, codebase knowledge. Do NOT use save_memory for: credential values (use manage_credentials), confirmed vulnerabilities (use manage_findings), attack hypotheses (use manage_assessments).

save_memory(

content="Auth mechanism uses JWT with RS256. Token expiry is 1 hour. Refresh token stored in httpOnly cookie.",
title="JWT auth mechanism details", # Optional: short title
memory_type="discovery", # Optional: discovery, learning, warning, error_fix, decision, codebase_knowledge
references=["service://1", "endpoint://42"] # Optional: entity IDs in "type://id" format
# Valid reference types: endpoint, service, technology, flow, account, credential,
# finding, assessment, attack_chain, memory, agent, task
)

query_memories

Query saved memories using semantic search.

memories = query_memories(
query="SQL injection authentication",
memory_type="discovery", # Optional filter (single type, not list)
limit=10
)

Common Patterns

Duplicate Prevention

Always check before creating to avoid duplicates:

# Check for existing endpoints before creating
all_endpoints = manage_endpoints(action="list")
existing = [e for e in all_endpoints.get("endpoints", []) if target_url in e.get("url", "")]

if not existing:
manage_endpoints(action="create", url=target_url, ...)

Reflection Pattern (Discovery Audit)

After each phase, audit surfaces and flows discovered:

# For each surface touched
for surface in surfaces_touched:
all_endpoints = manage_endpoints(action="list")
existing = [e for e in all_endpoints.get("endpoints", []) if surface["url"] in e.get("url", "")]

if not existing:
# NEW SURFACE - create endpoint and spawn investigation task
manage_endpoints(action="create", url=surface["url"], ...)
manage_tasks(action="create", phase_id=2, ...)

# For each flow observed
for flow in flows_observed:
all_flows = manage_flows(action="list_flows")
existing = [f for f in all_flows.get("flows", []) if flow["name"] in f.get("name", "")]

if not existing:
# NEW FLOW - create flow and spawn investigation task
manage_flows(action="create_flow", name=flow["name"], ...)
manage_tasks(action="create", phase_id=3, ...)

Task Completion Pattern

When finishing a task:

# 1. Save completion memory
save_memory(

memory_type="discovery",
content=f"Task complete: {summary}. Key findings: {findings}"
)

# 2. Mark task done with learnings
manage_tasks(
action="update_status",

task_id=TASK_ID,
status="done",
summary=f"Investigated {target}: {result}",
key_learnings=[
f"Technique: {technique_used}",
f"Result: {outcome}"
]
)

Writing Entity Descriptions

Entity descriptions are the primary way other agents and humans understand what a service, endpoint, or flow actually does. A bad description wastes everyone's time. A good description prevents duplicate work and grounds security analysis.

What belongs in a description

Describe what the entity does functionally — its purpose, inputs, outputs, and behavior. Write for someone who has never seen this application.

EntityGood descriptionBad description
Service"OAuth2/OIDC auth service. Handles login, token issuance, session management. RS256 JWT, 1h expiry. Endpoints: /authorize, /token, /userinfo.""Authentication microservice"
Endpoint"Full-text user search returning paginated results with PII (email, role, last login). Requires any authenticated session. No input length limit on query. Rate-limited to 10 req/min.""User search endpoint"
Flow"Self-service registration: submit email+password → verification email with signed token → click to activate. Creates 'member' role. No admin approval. Email uniqueness at DB level.""Complete user registration flow"

For endpoints, put request/response parameters and types in openapi_schema. Keep description focused on functional purpose, auth requirements, data sensitivity, and security-relevant behavior.

What does NOT belong in a description

Specific discoveries and commentary belong in save_memory(references=["endpoint://42"]). Descriptions are for what the entity is, not what you learned about it.

Updating descriptions

Always read before writing. Call GET first, read the current description, then either:

  • Append genuinely new functional detail you discovered (e.g., rate limits, auth requirements, undocumented parameters)
  • Leave it alone if your update is commentary — use save_memory(references=[...]) instead

Never replace a detailed description with a shorter one. Never use descriptions as a scratch pad.


Important Notes

  1. MCP tools are the PRIMARY knowledge store - All discovered services, endpoints, accounts, findings, and observations MUST be stored via MCP tools. Markdown files are only for supplementary reports (e.g., report_path on findings) and scratch notes. Data only in files is invisible to other agents and the platform.
  2. agent_id is automatic - Your identity is resolved from HTTP headers, no need to pass it
  3. Use list + filter instead of search - Most tools support list action; filter results in memory
  4. Save memories for observations and learnings - Other agents can query these later
  5. Use the right entity for the right data:
    • Discovered services/apps → manage_services
    • Discovered endpoints/URLs → manage_endpoints
    • Discovered user flows → manage_flows
    • Discovered technologies → manage_services(action="add_technology")
    • Discovered accounts → manage_accounts
    • Credentials (passwords, JWTs, API keys) → manage_credentials
    • Attack hypotheses → manage_assessments
    • Confirmed vulnerabilities → manage_findings
    • Observations and learnings → save_memory
  6. Check before creating - Avoid duplicate endpoints, flows, accounts, etc.
  7. Complete tasks with learnings - Always include summary and key_learnings when marking done
  8. Read before updating descriptions - Always GET an entity and read its current description before updating. Never blindly overwrite. See "Writing Entity Descriptions" above.