The MCP proxy needs two things to enforce a vendor MCP server's tools : a policy mapping (which tool calls what Knowledge action) and fetchers (how to populate required_context fields when Knowledge asks for them).
This guide walks through wiring a hypothetical vendor MCP server that exposes search_kb, create_ticket, and close_ticket.
Step 1 : audit the vendor's tools
knowledge-mcp-proxy inspect \
--transport stdio \
--command "npx -y @vendor/some-mcp-server" \
--tools-list
Output :
{
"tools": [
{ "name": "search_kb", "inputSchema": { "properties": { "query": { "type": "string" } } } },
{ "name": "create_ticket", "inputSchema": { "properties": { "title": {...}, "priority": {...}, "assignee": {...} } } },
{ "name": "close_ticket", "inputSchema": { "properties": { "ticket_id": {...} } } }
]
}
Step 2 : map each tool to a Knowledge action
proxy.yaml :
policy_mapping:
"search_kb":
action: kb.search
resource: "{query}"
bind: []
"create_ticket":
action: ticket.create
resource: "{title}"
bind: [priority, assignee]
"close_ticket":
action: ticket.close
resource: "{ticket_id}"
bind: []
Guidance :
action: hierarchical name. Reuse the same action across tools that mean the same thing (aclose_ticketin Zendesk and aclose_ticketin ServiceNow can shareticket.closeif the policies are the same).resource: identifies what is being acted on. Interpolate from tool args with{arg_name}. Match to what the rule scope references.bind: arg names whose values become part of the signed envelope. Any operation whose bound arg value differs will be refused by the PEP. Bind the fields your rules discriminate on.
Step 3 : write the Knowledge rules
In the back-office UI (or via seed file), create rules that cite the actions and scope fields from your mapping :
- id: rul-tickets-critical-restricted
policy: pol-ticketing
statement: "Only 'senior' support agents can create critical tickets."
severity: hard_block
rows:
- scope: { action: ticket.create }
condition:
parameters.priority: { eq: critical }
caller.seniority: { neq: senior }
Step 4 : register fetchers for required_context
If a rule references a field not present in the tool call (e.g. caller.seniority above), Knowledge returns required_context. The proxy needs a fetcher.
Create my_org/fetchers.py :
def get_caller_seniority(context: dict) -> dict:
caller_id = context["caller"]["id"]["value"]
seniority = hr.lookup(caller_id).level
return {"value": seniority, "source": "hr_system"}
def get_client_verified_flag(context: dict) -> dict:
kyc_id = context["client"]["kyc_id"]["value"]
return {"value": kyc.is_verified(kyc_id), "source": "kyc_vendor"}
Wire them in proxy.yaml :
fetchers:
module: my_org.fetchers
registry:
"caller.seniority": get_caller_seniority
"client.verified_flag": get_client_verified_flag
Step 5 : verify end-to-end
Happy path :
call: create_ticket(title="Login broken", priority=low, assignee=hum-bob)
proxy: consults Knowledge -> allowed -> forwards to upstream -> ticket created
Refused path (bind mismatch) :
Vendor tool returns a downstream error trying to elevate priority from low to critical post-facto. PEP refuses with binding_mismatch : the signed envelope authorised priority=low, not critical.
Progressive context path :
call: create_ticket(title="Server down", priority=critical, assignee=hum-alice)
proxy: /resolve -> required_context: [caller.seniority]
proxy: fetcher get_caller_seniority(ctx) -> {value: "junior", source: "hr_system"}
proxy: /resolve again -> blocked, cited_rules: [rul-tickets-critical-restricted]
client: sees typed MCP error
Common pitfalls
- Forgetting to bind a discriminating field. If
priorityis a rule discriminator but not inbind, the PEP has no signature commitment on it - an attacker (or a misbehaving upstream) could change it. Always bind fields your rules read. allow_unmapped: true(default) hides new tools. If the vendor adds a new tool in an update, it passes ungoverned. Set tofalsein compliance-critical deployments.- Fetchers that block on slow APIs. Fetchers run in the proxy's request path ; keep them fast or cache aggressively. If a fetch legitimately takes minutes, use a background job +
approval_required.
Related
- Setup - install + first run.
- Config reference - every knob.
- Progressive context - the loop the fetchers feed.
