Pter MCP Tool Reference
This document provides a comprehensive reference for the Pter MCP tools available to agents.
Quick Reference Table
| Tool | Actions | Purpose |
|---|---|---|
manage_services | create, update, get, list, add_technology | Service registry management |
manage_endpoints | create, update, get, list | API endpoint tracking |
manage_flows | create_flow, update_flow, get_flow, list_flows, delete_flow | User journey flows |
manage_auth_session | list_sessions, get_current_session, replace_current_session, create_new_session, reauth, set_metadata, get_metadata, add_api_key, list_api_keys, update_capabilities, update_description | Authentication session management |
manage_assessments | create, update, get, list, delete, submit_result | Security hypothesis tracking |
manage_findings | create, update, get, list, delete, statistics | Validated security findings |
manage_credentials | create, list, get, update, delete, list_by_account, update_status | Discovered credentials |
manage_tasks | create, get_details, update_status | Task lifecycle management |
save_memory | N/A (single action) | Save knowledge to RAG |
query_memories | N/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 technologychain— 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:
- What — the vulnerability class and root cause (not just the symptom)
- Where — exact endpoint, parameter, function, or component
- Why it matters — concrete impact in context of this application (data exposure, account takeover, lateral movement — not generic risk language)
- 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:
| Score | Severity |
|---|---|
| 9.0 – 10.0 | critical |
| 7.0 – 8.9 | high |
| 4.0 – 6.9 | medium |
| 0.1 – 3.9 | low |
| 0.0 | informational |
# 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.
recommended_fix
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.
| Entity | Good description | Bad 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
- 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_pathon findings) and scratch notes. Data only in files is invisible to other agents and the platform. - agent_id is automatic - Your identity is resolved from HTTP headers, no need to pass it
- Use list + filter instead of search - Most tools support
listaction; filter results in memory - Save memories for observations and learnings - Other agents can query these later
- 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
- Discovered services/apps →
- Check before creating - Avoid duplicate endpoints, flows, accounts, etc.
- Complete tasks with learnings - Always include summary and key_learnings when marking done
- Read before updating descriptions - Always GET an entity and read its current description before updating. Never blindly overwrite. See "Writing Entity Descriptions" above.