Intent-aware destination verification for AI agents...
PreClick is an intent-aware secure browsing and decision layer for AI agents. Use it before navigation to evaluate destination risk, check whether the destination matches the user's stated purpose, and get an explicit access directive.
Configure the PreClick MCP server in your MCP client:
{
"mcpServers": {
"preclick-mcp": {
"transport": "streamable-http",
"url": "https://preclick.ai/mcp"
}
}
}
Hosted trial access does not require an API key for up to 100 requests/day. For higher limits, send X-API-Key.
PreClick exposes six public tools:
| Tool | Purpose | Best fit |
|---|---|---|
url_scanner_scan |
Scan a URL without intent context | Native Tasks clients |
url_scanner_scan_with_intent |
Scan a URL with user intent context | Native Tasks clients |
url_scanner_async_scan |
Submit a scan and get task_id immediately |
Clients without native Tasks |
url_scanner_async_scan_with_intent |
Submit an intent-aware scan and get task_id immediately |
Clients without native Tasks |
url_scanner_async_task_status |
Poll async task status | Clients without native Tasks |
url_scanner_async_task_result |
Fetch async task result | Clients without native Tasks |
Use an async workflow by default. Typical scan time is around 70-80 seconds on current production traffic.
Choose the approach that matches the MCP client:
tasks/* methodstask on tools/call and can call tasks/get and tasks/resultFor both approaches, use the same high-level loop:
agent_access_directive, not risk_score aloneDo not block the user silently while waiting. Show progress updates during polling.
This is the safest default for agents that do not implement native MCP Tasks.
| Tool | When to use |
|---|---|
url_scanner_async_scan |
No useful user intent is available |
url_scanner_async_scan_with_intent |
The user has stated their purpose, such as login, purchase, booking, payment, or download |
Standard scan:
{
"name": "url_scanner_async_scan",
"arguments": {
"url": "https://example.com"
}
}
Intent-aware scan:
{
"name": "url_scanner_async_scan_with_intent",
"arguments": {
"url": "https://example.com/login",
"intent": "Log into my account"
}
}
Expected submit response shape:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "working",
"status_message": "Queued for processing",
"created_at": "2026-01-18T12:00:00Z",
"updated_at": "2026-01-18T12:00:00Z",
"ttl_ms": 720000,
"poll_interval_ms": 2000,
"message": "Scan submitted. Poll with url_scanner_async_task_status or url_scanner_async_task_result."
}
Do not include a native MCP task parameter when calling any url_scanner_async_* tool.
Wait for poll_interval_ms, then poll status:
{
"name": "url_scanner_async_task_status",
"arguments": {
"task_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
Expected status response shape:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "working",
"status_message": "Queued for processing",
"created_at": "2026-01-18T12:00:00Z",
"updated_at": "2026-01-18T12:00:00Z",
"ttl_ms": 720000,
"poll_interval_ms": 2000
}
Status values mirror MCP task semantics:
workingcompletedfailedcancelledPreferred loop:
url_scanner_async_task_statusworking, wait poll_interval_ms and poll againcompleted, call url_scanner_async_task_resultfailed or cancelled, treat the task as unsuccessful and report a generic failure{
"name": "url_scanner_async_task_result",
"arguments": {
"task_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
If the task is still running, the result tool returns:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "working",
"status_message": "Queued for processing",
"result": null,
"retry_after_ms": 2000,
"message": "Scan still in progress. Call url_scanner_async_task_result again after retry_after_ms."
}
If the task is completed, the result tool returns:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"status_message": "Scan completed successfully",
"result": {
"risk_score": 0.15,
"confidence": 0.92,
"analysis_complete": true,
"agent_access_directive": "ALLOW",
"agent_access_reason": "no_immediate_risk_detected",
"intent_alignment": "not_provided"
},
"retry_after_ms": null,
"message": "Scan completed successfully."
}
If the underlying task failed, was cancelled, or expired, url_scanner_async_task_result returns an MCP error rather than a normal result payload.
Use this only when the client supports native task submission and polling.
Standard scan:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_scanner_scan",
"arguments": {
"url": "https://example.com"
},
"task": {
"ttl": 720000
}
}
}
Intent-aware scan:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_scanner_scan_with_intent",
"arguments": {
"url": "https://example.com/login",
"intent": "Log into my account"
},
"task": {
"ttl": 720000
}
}
}
Expected task handle shape:
{
"task": {
"taskId": "550e8400-e29b-41d4-a716-446655440000",
"status": "working",
"statusMessage": "Queued for processing",
"createdAt": "2026-01-18T12:00:00Z",
"lastUpdatedAt": "2026-01-18T12:00:00Z",
"ttl": 720000,
"pollInterval": 2000
}
}
tasks/get{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/get",
"params": {
"taskId": "550e8400-e29b-41d4-a716-446655440000"
}
}
Expected status response shape:
{
"taskId": "550e8400-e29b-41d4-a716-446655440000",
"status": "working",
"statusMessage": "Queued for processing",
"createdAt": "2026-01-18T12:00:00Z",
"lastUpdatedAt": "2026-01-18T12:00:00Z",
"ttl": 720000,
"pollInterval": 2000
}
Preferred loop:
tasks/getworking, wait pollInterval and poll againcompleted, call tasks/resultfailed or cancelled, treat the task as unsuccessful and report a generic failure{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/result",
"params": {
"taskId": "550e8400-e29b-41d4-a716-446655440000"
}
}
Successful tasks/result responses use the normal CallToolResult shape:
{
"content": [
{
"type": "text",
"text": "{\"risk_score\":0.15,\"confidence\":0.92,\"analysis_complete\":true,\"agent_access_directive\":\"ALLOW\",\"agent_access_reason\":\"no_immediate_risk_detected\",\"intent_alignment\":\"not_provided\"}"
}
],
"isError": false
}
Important native-task caveat:
tasks/result can wait, but on the hosted deployment it uses a shorter wait timeout than direct callstasks/result returns JSON-RPC -32603 with error.data.taskId and error.data.pollIntervaltasks/get and retry tasks/result only after the task is completedThe final decision fields are the same across both approaches, but the envelope differs:
resultcontent[0].textNormalize to these fields before making a decision:
risk_scoreconfidenceanalysis_completeagent_access_directiveagent_access_reasonintent_alignmentAlways use agent_access_directive as the primary decision field.
| Directive | Meaning |
|---|---|
ALLOW |
Safe to proceed |
DENY |
Do not navigate |
RETRY_LATER |
Temporary failure; retry after a delay |
REQUIRE_CREDENTIALS |
Destination requires authentication |
risk_score is supplementary detail. Do not treat it as the final access decision by itself.
When using an intent-aware tool, the result includes intent_alignment:
| Value | Meaning |
|---|---|
misaligned |
Page purpose conflicts with the stated intent |
no_mismatch_detected |
No mismatch signal detected from available evidence |
inconclusive |
Evidence is limited or intent cannot be verified reliably |
not_provided |
No intent was supplied (or low-information input was ignored) |
When intent_alignment is misaligned, agent_access_directive is set to DENY even if risk_score is low.
Use the intent-aware variant when the user states a concrete purpose:
url_scanner_async_scan_with_intenturl_scanner_scan_with_intentIntent examples:
"Log into my account""Pay an invoice""Download a document""Book a reservation"Intent max length is 248 characters. Low-information strings such as "test" or "n/a" are treated as not provided.
Submit a scan:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_scanner_async_scan",
"arguments": {
"url": "https://example.com"
}
}
}'
Poll status:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "url_scanner_async_task_status",
"arguments": {
"task_id": "YOUR_TASK_ID"
}
}
}'
Fetch final result:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "url_scanner_async_task_result",
"arguments": {
"task_id": "YOUR_TASK_ID"
}
}
}'
Start a task-augmented scan:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_scanner_scan",
"arguments": {
"url": "https://example.com"
},
"task": {
"ttl": 720000
}
}
}'
Poll task status:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/get",
"params": {
"taskId": "YOUR_TASK_ID"
}
}'
Fetch final result:
curl -X POST https://preclick.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/result",
"params": {
"taskId": "YOUR_TASK_ID"
}
}'
risk_score alone for decisions instead of agent_access_directiveurl_scanner_async_* tools with a native MCP task parametertasks/result as the primary polling loop instead of tasks/gettasks/result returns a CallToolResult envelope, not a direct result objecttask parameter when using native Tasks on HTTP transport-32603-32029tasks/result may return JSON-RPC -32603 when its wait timeout expires before completiontask may time out after the hosted wait window; prefer async workflows