MCP Server: Stateless Tool Calls with Protocol 2026-07-28
This example demonstrates the stateless MCP protocol (revision 2026-07-28) for AI/LLM integration.
Protocol used here:
Stateless MCP protocol, revision 2026-07-28 - no initialize/initialized handshake and no Mcp-Session-Id header. Each request stands on its own:
- In the body, params._meta carries "io.modelcontextprotocol/protocolVersion", "io.modelcontextprotocol/clientCapabilities" and optionally "io.modelcontextprotocol/clientInfo".
- In the headers: Mcp-Protocol-Version, Mcp-Method (must equal the JSON-RPC method), and Mcp-Name for tools/call (the tool name) and resources/read (the resource URI).
No session is created: responses carry no Mcp-Session-Id header, and a stale Mcp-Session-Id sent by a mixed-version client is ignored when _meta says 2026-07-28 or later.
New method:
- server/discover - returns the server's capabilities and the protocol revisions it accepts (result.supportedVersions), so a client can decide which style to speak.
Workflow:
1. Call server/discover to confirm 2026-07-28 is supported
2. List available tools (authzen_evaluate, ciq_execute) - statelessly
3. Execute tools with parameters, one self-contained request per call
Requesting an unsupported revision returns HTTP 400 with JSON-RPC error -32022 ("unsupported protocol version") naming the requested and supported versions.
The session-based flow (see the companion MCP resource) keeps working unchanged - both styles expose the same tools and resources.
Use case
Scenario: A serverless AI agent (one short-lived invocation per user question) needs to check permissions and retrieve authorized data - holding an MCP session between invocations is impractical.
Stateless integration flow:
1. Agent cold-starts, calls server/discover -> sees "2026-07-28" in supportedVersions
2. User asks: "Can I drive the company car?"
3. Agent sends one self-contained tools/call request:
- Headers: Mcp-Protocol-Version: 2026-07-28, Mcp-Method: tools/call, Mcp-Name: authzen_evaluate
- Body params: the authzen_evaluate arguments plus the _meta protocol object
4. MCP evaluates against the KBAC policy and returns {decision: true}
5. The invocation ends - nothing to tear down, no session to expire
For data retrieval the same shape applies: one stateless tools/call to ciq_execute with the knowledge query ID and input_params, authorized by the caller's Bearer token.

Requirements
Prerequisites (identical environment to the session-based MCP resource):
- ServiceAccount credentials: For creating policies, queries, and the MCP Server configuration (Bearer token)
- AppAgent: Referenced by the MCP Server config as the identity the server uses to call IndyKite APIs at runtime (resolved server-side)
- User access token: JWT for the subject performing authorization checks
- Token Introspect configuration: Required to validate user access tokens (referenced by the MCP Server config)
MCP Server endpoint:
- URL: https://eu.mcp.indykite.com/mcp/v1/{project_gid}
- Protocol: JSON-RPC 2.0 over HTTP POST - stateless MCP protocol, revision 2026-07-28 (params._meta + Mcp-Protocol-Version / Mcp-Method / Mcp-Name headers; no session)
Required configurations:
- MCP Server: Binds the runtime MCP endpoint to an AppAgent + Token Introspect, declares supported OAuth scopes
- KBAC Policy: Defines authorization rules (e.g., who can drive cars)
- ContX IQ Policy and Query: Defines data access rules and queries
Steps
Step 1: Create the MCP Server configuration
- Authentication: ServiceAccount credential (Bearer token)
- Action: POST to /configs/v1/mcp-servers
- Input: AppAgent ID, Token Introspect ID, project ID, supported OAuth scopes
- Result: MCP Server config ID returned; the runtime MCP endpoint will use this binding
Step 2: Ingest Graph Data
- Authentication: AppAgent credential (X-IK-ClientKey header)
- Action: POST nodes (Person, Car) and relationships (DRIVES)
- Note: Replace bearer-token-sub with actual user's token subject claim
- Result: Graph ready for authorization queries
Step 3: Create ContX IQ Policy
- Authentication: ServiceAccount credential (Bearer token)
- Action: POST policy for data access through MCP
- Result: Policy ID returned
Step 4: Create ContX IQ Knowledge Query
- Authentication: ServiceAccount credential (Bearer token)
- Action: POST query with descriptive name and parameters
- Important: Include detailed description for AI agent understanding
- Result: Query ID available via MCP resources
Step 5: Create KBAC Policy
- Authentication: ServiceAccount credential (Bearer token)
- Action: POST authZEN policy (e.g., Person can DRIVE Car if DRIVES relationship exists)
- Result: Policy ID returned, available via authzen_evaluate tool
Step 6: Use the MCP Server statelessly (protocol 2026-07-28)
- Discover: "server/discover" returns capabilities and supportedVersions - no session is created
- List tools: "tools/list" with the _meta object and Mcp-Method header
- Read the knowledge-queries resource: "resources/read" with Mcp-Name set to the resource URI
- Execute authzen_evaluate: one self-contained "tools/call" with Mcp-Name: authzen_evaluate
- Execute ciq_execute: one self-contained "tools/call" with Mcp-Name: ciq_execute
- Every request repeats the Bearer token and the _meta protocol object; no Mcp-Session-Id anywhere
Step 1
Create the MCP Server configuration. This binds the runtime MCP endpoint (https://<region>.mcp.indykite.com/mcp/v1/<project_gid>) to an AppAgent (the identity the server uses to call IndyKite APIs at runtime, resolved server-side) and a Token Introspect config (used to validate Bearer tokens), and declares the OAuth scopes the server advertises in its .well-known/oauth-protected-resource metadata. Replace gid-of-app-agent and gid-of-token-introspect with the IDs returned in the environment-setup steps.
{
"app_agent_id": "gid-of-app-agent",
"token_introspect_id": "gid-of-token-introspect",
"project_id": "gid-of-project",
"name": "mcp-server-name",
"display_name": "MCP Server name",
"description": "MCP Server configuration description",
"enabled": true,
"scopes_supported": [
"name",
"email"
]
}Step 2
Capture the nodes needed for this use case (replace bearer-token-sub with actual Bearer token sub).
{
"nodes": [
{
"external_id": "bearer-token-sub",
"is_identity": true,
"type": "Person",
"properties": [
{
"type": "email",
"value": "alice@email.com"
},
{
"type": "given_name",
"value": "Alice"
},
{
"type": "last_name",
"value": "Smith"
}
]
},
{
"external_id": "knightrider",
"type": "Person",
"is_identity": true,
"properties": [
{
"type": "email",
"value": "knightrider@demo.com"
},
{
"type": "name",
"value": "Michael Knight"
}
]
},
{
"external_id": "satchmo",
"type": "Person",
"is_identity": true,
"properties": [
{
"type": "email",
"value": "satchmo@demo.com"
},
{
"type": "name",
"value": "Louis Armstrong"
}
]
},
{
"external_id": "karel",
"type": "Person",
"is_identity": true,
"properties": [
{
"type": "email",
"value": "karel@demo.com"
},
{
"type": "name",
"value": "Karel Plihal"
}
]
},
{
"external_id": "kitt",
"type": "Car",
"is_identity": false,
"properties": [
{
"type": "manufacturer",
"value": "pontiac"
},
{
"type": "model",
"value": "Firebird"
}
]
},
{
"external_id": "cadillacv16",
"type": "Car",
"is_identity": false,
"properties": [
{
"type": "manufacturer",
"value": "Cadillac"
},
{
"type": "model",
"value": "V-16"
}
]
},
{
"external_id": "harmonika",
"type": "Bus",
"is_identity": false,
"properties": [
{
"type": "manufacturer",
"value": "Ikarus"
},
{
"type": "model",
"value": "280"
}
]
},
{
"external_id": "listek",
"type": "Ticket",
"is_identity": false
},
{
"external_id": "airbook-xyz",
"type": "Laptop",
"is_identity": false
},
{
"external_id": "ole",
"is_identity": true,
"type": "Person",
"properties": [
{
"type": "email",
"value": "ole@yahoo.co.uk"
},
{
"type": "given_name",
"value": "ole"
},
{
"type": "last_name",
"value": "einar"
}
]
},
{
"external_id": "cb2563",
"type": "PaymentMethod",
"properties": [
{
"type": "payment_name",
"value": "Credit Card Parking"
},
{
"type": "preference",
"value": "Pay as you go"
}
]
},
{
"external_id": "carOle",
"type": "Vehicle",
"properties": [
{
"type": "category",
"value": "Car"
},
{
"type": "is_active",
"value": true
},
{
"type": "vin",
"value": "pcfjnm78"
}
]
},
{
"external_id": "licenseOle",
"type": "LicenseNumber",
"properties": [
{
"type": "status",
"value": "Active"
},
{
"type": "number",
"value": "AL98745",
"metadata": {
"assurance_level": 3,
"source": "BRREG"
}
}
]
},
{
"external_id": "licenseAlice",
"type": "LicenseNumber",
"properties": [
{
"type": "status",
"value": "Active"
},
{
"type": "number",
"value": "BTYUMN",
"metadata": {
"assurance_level": 3,
"source": "BRREG"
}
}
]
},
{
"external_id": "loyalty1",
"type": "Loyalty",
"properties": [
{
"type": "name",
"value": "Parking Loyalty Plan"
}
]
},
{
"external_id": "consent1",
"type": "ConsentPayment",
"properties": [
{
"type": "name",
"value": "Consent Parking"
}
]
},
{
"external_id": "companyParking",
"type": "Company",
"properties": [
{
"type": "name",
"value": "City Parking Inc"
}
]
},
{
"external_id": "applicationParking",
"type": "Application",
"properties": [
{
"type": "name",
"value": "City Mall Parking"
}
]
}
]
}Step 2
Capture the relationships needed for this use case (replace bearer-token-sub with actual Bearer token sub).
{
"relationships": [
{
"source": {
"external_id": "knightrider",
"type": "Person"
},
"target": {
"external_id": "kitt",
"type": "Car"
},
"type": "DRIVES"
},
{
"source": {
"external_id": "satchmo",
"type": "Person"
},
"target": {
"external_id": "cadillacv16",
"type": "Car"
},
"type": "DRIVES"
},
{
"source": {
"external_id": "karel",
"type": "Person"
},
"target": {
"external_id": "listek",
"type": "Ticket"
},
"type": "HAS"
},
{
"source": {
"external_id": "listek",
"type": "Ticket"
},
"target": {
"external_id": "harmonika",
"type": "Bus"
},
"type": "FOR"
},
{
"source": {
"external_id": "karel",
"type": "Person"
},
"target": {
"external_id": "airbook-xyz",
"type": "Laptop"
},
"type": "OWNS"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "airbook-xyz",
"type": "Laptop"
},
"type": "OWNS"
},
{
"source": {
"external_id": "knightrider",
"type": "Person"
},
"target": {
"external_id": "kitt",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "cadillacv16",
"type": "Car"
},
"type": "DRIVES"
},
{
"source": {
"external_id": "ole",
"type": "Person"
},
"target": {
"external_id": "cb2563",
"type": "PaymentMethod"
},
"type": "HAS"
},
{
"source": {
"external_id": "ole",
"type": "Person"
},
"target": {
"external_id": "carOle",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "ole",
"type": "Person"
},
"target": {
"external_id": "loyalty1",
"type": "Loyalty"
},
"type": "IS_MEMBER"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "loyalty1",
"type": "Loyalty"
},
"type": "IS_MEMBER"
},
{
"source": {
"external_id": "ole",
"type": "Person"
},
"target": {
"external_id": "consent1",
"type": "ConsentPayment"
},
"type": "GRANTED"
},
{
"source": {
"external_id": "carOle",
"type": "Car"
},
"target": {
"external_id": "licenseOle",
"type": "LicenseNumber"
},
"type": "HAS"
},
{
"source": {
"external_id": "consent1",
"type": "ConsentPayment"
},
"target": {
"external_id": "cb2563",
"type": "PaymentMethod"
},
"type": "GRANTED"
},
{
"source": {
"external_id": "companyParking",
"type": "Company"
},
"target": {
"external_id": "applicationParking",
"type": "Application"
},
"type": "OWNS"
},
{
"source": {
"external_id": "applicationParking",
"type": "Application"
},
"target": {
"external_id": "consent1",
"type": "ConsentPayment"
},
"type": "USES"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "cb8521",
"type": "PaymentMethod"
},
"type": "HAS"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "carAlice",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "bearer-token-sub",
"type": "Person"
},
"target": {
"external_id": "consent1",
"type": "ConsentPayment"
},
"type": "GRANTED"
},
{
"source": {
"external_id": "carAlice",
"type": "Car"
},
"target": {
"external_id": "licenseAlice",
"type": "LicenseNumber"
},
"type": "HAS"
},
{
"source": {
"external_id": "consent1",
"type": "ConsentPayment"
},
"target": {
"external_id": "cb8521",
"type": "PaymentMethod"
},
"type": "GRANTED"
}
]
}Step 3
Create a CIQ Policy which designates the Subject node, the cypher and the nodes allowed to be read.
{
"project_id": "your_project_gid",
"description": "description of policy",
"display_name": "policy name",
"name": "policy-name",
"policy": "{\"policy\":{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person) MATCH (app:Application)-[:USES]->(consentpayment:ConsentPayment)<-[:GRANTED]-(subject)-[:HAS]->(paymentmethod:PaymentMethod) MATCH (subject)-[:IS_MEMBER]->(loyalty:Loyalty) MATCH (subject)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"subject.property.email\",\"operator\":\"=\",\"value\":\"$subject_email\"}]}]},\"allowed_reads\":{\"nodes\":[\"ln.*\",\"app.*\",\"paymentmethod.external_id\"]}}}",
"status": "ACTIVE",
"tags": []
}Step 4
Create a CIQ Query in the context of the policy. In the description, give all the necessary information an agent would need to know to call the ciq_execute tool.
{
"project_id": "your_project_gid",
"description": "Call tool 'ciq_execute' with arguments : id: \"<this query's id>\", input_params: {subject_external_id: (required) must match Bearer token 'sub', subject_email: (required), license: (required) car license plate}. Auth: Bearer token required, token subject = subject_external_id. Returns: payment_method_external_id",
"display_name": "knowledge query name",
"name": "knowledge-query-name",
"policy_id": "your_policy_gid",
"query": "{\"nodes\":[\"paymentmethod.external_id\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$license\"}}",
"status": "ACTIVE"
}Step 5
Create a KBAC Policy which designates the conditions to drive a car.
{
"project_id": "your_project_gid",
"description": "description of policy",
"display_name": "policy name",
"name": "policy-name",
"policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}",
"status": "ACTIVE",
"tags": []
}Step 6
Discover the server. server/discover is a stateless-protocol method: it returns the server's capabilities and the protocol revisions it accepts, without creating a session. Note the _meta object in params and the Mcp-Protocol-Version / Mcp-Method headers - the stateless request signature used by every call below.
curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/<project_gid> -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"}
}
}
}'server/discover response: the supported protocol revisions and capabilities. No Mcp-Session-Id header is returned - no session exists.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": {"name": "<server name>", "version": "<server version>"}
},
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"],
"capabilities": {
"resources": {},
"tools": {}
},
"instructions": "<server instructions for the AI agent>"
}
}List tools statelessly. Same tools as the session-based flow (authzen_evaluate, authzen_evaluations, authzen_search_action, authzen_search_resource, ciq_execute) - only the request framing differs.
curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/<project_gid> -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/list" -d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Read the knowledge-queries resource statelessly. For resources/read the Mcp-Name header carries the resource URI and must match the uri in the body.
curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/<project_gid> -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: resources/read" -H "Mcp-Name: indykite://knowledge-queries/" -d '{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/read",
"params": {
"uri": "indykite://knowledge-queries/",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Call the authzen_evaluate tool in one self-contained request: Mcp-Method mirrors the JSON-RPC method, Mcp-Name names the tool, and _meta sits in params next to the tool arguments. The KBAC policy decides whether the Bearer token's subject can drive the car.
curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/<project_gid> -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: authzen_evaluate" -d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "authzen_evaluate",
"arguments": {
"subject_type": "Person",
"subject_id": <bearer-token-sub>,
"resource_type": "Car",
"resource_id": "cadillacv16",
"action_name": "CAN_DRIVE"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Authzen_evaluate tool response - identical to the session-based flow, with no Mcp-Session-Id header.
{
"jsonrpc":"2.0",
"id":4,
"result":{
"content":[
{
"type":"text",
"text": {"decision":true}
}
]
}
}Call the ciq_execute tool statelessly to get the payment method designated by the CIQ policy for the subject authorized by the Bearer access token.
curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/<project_gid> -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: ciq_execute" -d '{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "ciq_execute",
"arguments": {
"id": "<knowledge_query_id>",
"input_params": {"license": "BTYUMN","subject_external_id": <bearer-token-sub>,"subject_email": "alice@email.com"}
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Error contract: requesting a protocol revision the server does not support returns HTTP 400 with JSON-RPC error -32022, naming the requested and supported versions.
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32022,
"message": "unsupported protocol version",
"data": {
"requested": "2099-01-01",
"supported": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]
}
}
}API Endpoints
/configs/v1/mcp-servers/capture/v1/nodes/capture/v1/relationships/configs/v1/authorization-policies/configs/v1/knowledge-queries