How to define MCP tools in AnythingMCP: parameters, endpoint mapping, and response mapping.
Every MCP tool in AnythingMCP is defined by three JSON objects:
parameters— What the AI can pass as input (JSON Schema)endpointMapping— How parameters map to the API requestresponseMapping— (Optional) How to transform the API response
The catalog validator requires each adapter to provide slug, name,
description, region, category, icon, docsUrl, requiredEnvVars,
connector, and a non-empty tools array. The filename must match slug.
Optional environment variables use the same array-of-strings shape and must
not duplicate a required variable.
Keep the required envelope fields above at the adapter root; use arrays for
requiredEnvVars and optionalEnvVars, and do not list one variable in both.
When a variable is renamed, list its previous names under envVarAliases,
keyed by the current name: { "IS24_CONSUMER_KEY": ["IS24_CLIENT_ID"] }.
Connectors installed before the rename still hold the old name; saving their
environment variables reads it when the current one is not set, instead of
leaving an unresolved {{IS24_CONSUMER_KEY}} in the credentials.
Each entry in tools needs a string name, a useful description, and its
JSON-Schema parameters when it accepts input. Parameter properties should
include descriptions for the model. "enabled": false installs a tool switched
off, for writes a workspace should turn on deliberately.
See connector configuration and authentication for the nested connector fields.
Set connector.type to REST, GRAPHQL, SOAP, MCP, DATABASE, or
LOGIN_TOKEN. Set connector.authType to a supported authentication scheme
listed below; these values are validated before an adapter can pass.
Use NONE, API_KEY, BEARER_TOKEN, BASIC, BASIC_AUTH, OAUTH2,
OAUTH1, LOGIN_TOKEN, QUERY_AUTH, CONNECTION_STRING or HMAC as
connector.authType. Keep the corresponding credentials in authConfig and
reference environment variables with {{VAR}} where the connector injects
them.
For OAUTH2, authConfig.tokenAuthMethod says how the client authenticates
at the token endpoint: client_secret_post (default), client_secret_basic,
or private_key_jwt, which signs a short-lived client assertion with a private
key instead of sending a secret (see
REST connectors).
Some APIs never receive the secret: each request carries a digest computed
over a canonical string. authType: "HMAC" describes that string in the
adapter rather than in per-vendor engine code. Signing happens after the body
and query are built, because the canonical string usually folds them in.
"authType": "HMAC",
"authConfig": {
"signature": {
"algorithm": "sha256",
"encoding": "hex",
"secret": "{{KAUFLAND_SECRET_KEY}}",
"template": "${method}\n${url}\n${body}\n${timestamp}\n",
"headerName": "Shop-Signature",
"timestampHeader": "Shop-Timestamp",
"extraHeaders": { "Shop-Client-Key": "{{KAUFLAND_CLIENT_KEY}}" }
}
}template may use ${method}, ${url}, ${path} (path + query only),
${body} and ${timestamp} (Unix seconds), in whatever order the vendor
documents. \n is honoured. algorithm is sha256 (default), sha1 or
sha512; encoding is hex (default) or base64. timestampHeader sends
the same timestamp that went into the string, which the server needs to
recompute it.
The body is signed exactly as it will be sent — signing a different rendering of the same object is the usual way an HMAC integration fails with an error that blames the key.
When a vendor already runs an official MCP server, the adapter bridges it
instead of describing its API again. connector.type is MCP, baseUrl is
the server's MCP endpoint (variables allowed, e.g.
https://{{SPLUNK_HOST}}:8089/services/mcp), and authentication is whatever
the server accepts (usually BEARER_TOKEN).
At install AnythingMCP asks that server for its tools (tools/list) and
installs what it lists, so a workspace gets exactly the tools of the server
version it runs. The adapter's tools array is a snapshot: it is what the
catalog shows, the fallback installed when the server cannot be reached at
install, and the place for policy. A snapshot tool with "enabled": false
installs switched off, and annotations set there fill the hints the server
leaves out (the server's own hints win).
Each tool's endpointMapping is { "method": "<tool name>", "path": "/mcp" }.
Listing the tools is also the install check, so no probe is needed, and a
catalog update never rewrites or retires the tools of an MCP adapter: the
server owns them. See intl/splunk.json.
A DATABASE adapter points at a database instead of an HTTP API, so several
of the REST rules simply do not apply to it:
authTypeisCONNECTION_STRING.baseUrlis the DSN and carries the driver (postgres://,mysql://,mariadb://,mssql://,oracle://,mongodb://,sqlite://) — the engine picks the driver from that prefix.- Put the username and password in
authConfig, not in the DSN.authConfigis encrypted at rest;baseUrlis not. The engine splices them into the URL (Postgres, MySQL, Mongo) or passes them as driver config (MSSQL, Oracle, which also acceptauthConfig.domainfor NTLM). endpointMapping.methodisquery,static, ormongo_schema— never an HTTP verb.pathis the statement, not a URL.- A
pathof exactly${query}hands the engine the caller's raw SQL. Any otherpathis a template whose${name}placeholders are compiled to bound parameters, never string-interpolated. - Adapters install read-only; the engine rejects anything but a read until the user flips the switch in the connector's settings.
- Declare a
probe(a listing tool with no required parameters). A DATABASE adapter has no HTTP healthcheck, so without one the install reports nothing.
{
"slug": "postgres",
"requiredEnvVars": ["POSTGRES_HOST", "POSTGRES_PORT", "POSTGRES_DATABASE", "POSTGRES_USER", "POSTGRES_PASSWORD"],
"probe": { "tool": "postgres_list_tables" },
"connector": {
"name": "PostgreSQL",
"type": "DATABASE",
"authType": "CONNECTION_STRING",
"baseUrl": "postgres://{{POSTGRES_HOST}}:{{POSTGRES_PORT}}/{{POSTGRES_DATABASE}}",
"authConfig": {
"username": "{{POSTGRES_USER}}",
"password": "{{POSTGRES_PASSWORD}}"
}
},
"tools": [
{
"name": "postgres_query",
"description": "Run a read-only SQL SELECT against the database and return up to 1000 rows.",
"parameters": {
"type": "object",
"properties": { "query": { "type": "string", "description": "A single SQL SELECT statement." } },
"required": ["query"]
},
"endpointMapping": { "method": "query", "path": "${query}" }
}
]
}If an adapter file cannot be read, check that its path exists and that the validator process has permission to read it. This is distinct from invalid JSON syntax, which requires fixing the file contents.
Standard JSON Schema that defines tool inputs visible to the AI:
{
"type": "object",
"properties": {
"user_id": { "type": "integer", "description": "The user's ID" },
"include_details": { "type": "boolean", "description": "Include extra details" },
"query": { "type": "string", "description": "Search query" }
},
"required": ["user_id"]
}Supported types: string, integer, number, boolean, array, object
Tip: Parameters matching environment variable names are automatically stripped from the tool schema, so the AI never sees them.
The bridge configuration that transforms MCP tool calls into API requests.
{
"method": "POST",
"path": "/users/{user_id}/orders",
"queryParams": {
"page": "$page",
"limit": "$limit"
},
"bodyMapping": {
"productId": "$product_id",
"quantity": "$qty"
},
"headers": {
"X-Request-ID": "$request_id"
}
}| Pattern | Location | Description |
|---|---|---|
{param} in path |
URL path | Replaced in URL: /users/{id} becomes /users/123 |
"$param" in queryParams |
Query string | Added as ?param=value |
"$param" in bodyMapping |
JSON body | Included in request body |
"$param" in headers |
HTTP headers | Sent as request header |
"Bearer ${param}" in headers |
HTTP headers | Interpolated; the header is left out when any ${…} is empty or unset |
The $ prefix means "take the value from the tool input parameter with this name."
Connector variables are available the same way, so an optional key is written as
"headers": { "Authorization": "Bearer ${MY_API_KEY}" }: sent when the variable is set, omitted
when it is empty, rather than going out as Authorization: Bearer with nothing after it.
The same rule drops a whole string when one of its ${…} placeholders has no value. That matters
for APIs that take a query language in one field (Google Ads GAQL, SOQL, JQL): a missing optional
filter would drop the entire query. Give such parameters a JSON-Schema default that matches
everything, and the tool call fills it in before the string is built:
"parameters": { "properties": {
"campaign_name": { "type": "string", "default": "%" },
"limit": { "type": "integer", "default": 200 }
} },
"endpointMapping": { "method": "POST", "path": "/customers/{customer_id}/googleAds:search",
"bodyMapping": { "query": "SELECT campaign.name FROM campaign WHERE campaign.name LIKE '${campaign_name}' LIMIT ${limit}" } }A {param} value is inserted verbatim, slashes included, so a file path can span several segments. When
the identifier is itself a URL (a Search Console property such as https://www.example.com/, a sitemap
URL), set "encodePathParams": true on the mapping and every substituted value is percent-encoded
(https%3A%2F%2Fwww.example.com%2F). The model then passes the value raw, as the API returned it.
By default a tool receives the response body and nothing else. Some APIs put
the one thing a model needs to continue in a header instead: GitHub, GitLab,
Sentry and Shopify paginate with Link: <...?cursor=xyz>; rel="next", and most
APIs report rate limits in X-RateLimit-*. Without those, every list tool is
exactly one page long.
A REST tool can opt in per header name (case-insensitive):
{
"method": "GET",
"path": "/organizations/{{SENTRY_ORG}}/issues/",
"queryParams": { "cursor": "$cursor", "query": "$query" },
"exposeHeaders": ["link", "x-ratelimit-remaining"]
}The selected headers are added to the tool result next to the body, and a
Link header with rel="next" is parsed for you:
{
"...the body as before...": "",
"_headers": { "link": "<https://sentry.io/api/0/...?cursor=1568:0:0>; rel=\"next\"", "x-ratelimit-remaining": "39" },
"_pagination": { "nextUrl": "https://sentry.io/api/0/...?cursor=1568:0:0", "nextCursor": "1568:0:0", "cursorParam": "cursor" }
}_paginationis absent on the last page; tell the model so in the tool description ("call again withcursor=_pagination.nextCursoruntil it is missing").nextCursoris recognised for the usual parameter names (cursor,page,offset,after,page_token,starting_after, ...); otherwise onlynextUrlis set.- If the body is not a JSON object (an array, a string) it is wrapped as
dataso the extras have somewhere to live. - A response transform (
responseMapping.transform) runs on the body first; the extras are attached afterwards, so aselectcannot drop them. - The audit log keeps storing the bare body. Headers are cached together with it when
cacheTtlis set. - REST connectors only. Tools that did not set
exposeHeadersbehave exactly as before.
| Connector | method | path | queryParams | bodyMapping | headers |
|---|---|---|---|---|---|
| REST | HTTP method (GET, POST, etc.) |
URL path with {param} |
Query string params | JSON body fields | HTTP headers |
| OData | HTTP method, or a built-in odata_list_services / odata_describe_service / odata_describe_entity / odata_query / odata_get (see OData) |
Service path + entity set, with {param} |
OData options ($filter, $select…) |
JSON body fields | HTTP headers |
| GraphQL | query or mutation |
The GraphQL query string | GraphQL variables (see GraphQL variables) | — (ignored) | HTTP headers |
| SOAP | SOAP operation name | Port/binding path | — | SOAP parameters | HTTP headers |
| Database | query or static |
SQL/MongoDB query with $param |
— | — | — |
| MCP | Remote tool name | — | — | Passed through | — |
"method": "static" returns text stored on the tool, with no call to the upstream system: playbooks, example queries, domain guides. staticResponse is returned whatever the arguments. staticResponses holds several texts keyed by topic, chosen by the topic argument (or the argument named by topicParam); without a topic, or with an unknown one, the tool answers with staticResponse followed by the list of topics:
{
"name": "sap_guide",
"parameters": { "type": "object", "properties": { "topic": { "type": "string", "enum": ["basics", "finance"] } } },
"endpointMapping": {
"method": "static",
"path": "",
"staticResponse": "How to read the SAP data model…",
"staticResponses": { "basics": "Client, dates, currencies…", "finance": "The Universal Journal ACDOCA…" }
}
}{
"method": "query",
"path": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
"queryParams": {
"id": "$user_id"
}
}For generic tools that take the GraphQL operation as input, set path to a $paramName reference and use variablesFromParam to forward the variables map verbatim:
{
"method": "query",
"path": "$query",
"variablesFromParam": "variables"
}The GraphQL engine also supports "method": "static", which returns path verbatim with no HTTP call — useful for tools that just need to expose a fixed value (e.g. the URL of the SDL schema).
Every adapter with connector.type === "GRAPHQL" is automatically extended with four generic tools:
<slug>_graphql_schema_url— returns the URL of the SDL schema (default${baseUrl}/schema, override viaconnector.schemaUrl)<slug>_graphql_query— execute an arbitraryquery<slug>_graphql_mutation— execute an arbitrarymutation<slug>_graphql_subscription— execute an arbitrarysubscription(transport availability depends on the upstream API)
Adapter authors don't need to declare them.
The GraphQL engine builds the request's variables from exactly two places:
queryParams— one flat entry per variable, keyed by the variable name as the operation declares it, whose value is"$toolParam"or a literal;variablesFromParam— the whole map, taken from one tool parameter (generic tools only).
Nothing else is read. bodyMapping in particular is ignored, so "bodyMapping": { "variables": { … } } sends "variables": {} on every call, and a $param nested inside a queryParams object is sent as the literal text "$param". Where an API takes an input object, declare one variable per field and build the object inside the operation:
{
"method": "mutation",
"path": "mutation CreateCustomer($businessId: ID!, $name: String!, $email: String) { customerCreate(input: { businessId: $businessId, name: $name, email: $email }) { didSucceed } }",
"queryParams": { "businessId": "$business_id", "name": "$name", "email": "$email" }
}node scripts/validate-adapters.mjs enforces this for GRAPHQL adapters (rule graphql-variables): no bodyMapping, no nested $param in queryParams, every queryParams key declared by the operation, every non-null variable without a default supplied, and every "$name" a tool parameter or an env var. A REST connector that POSTs to a /graphql path builds its body with bodyMapping as usual and is not affected.
{
"method": "GetCustomerDetails",
"path": "CustomerServiceSoap12/BasicHttpBinding",
"bodyMapping": {
"customerId": "$customer_id",
"includeHistory": "$include_history"
}
}${name} placeholders are compiled to the driver's bound parameters
($1, ?, :1), so values are never spliced into the statement.
{
"method": "query",
"path": "SELECT * FROM orders WHERE customer_id = ${customer_id} AND status = ${status} ORDER BY created_at DESC LIMIT ${limit}"
}The path is a JSON find spec — collection plus optional filter,
projection, sort and limit. mongo_schema takes no path at all and
lists the collections with a sampled field list.
{
"method": "query",
"path": "{\"collection\": \"orders\", \"filter\": {\"customerId\": \"${customer_id}\"}, \"sort\": {\"createdAt\": -1}, \"limit\": ${limit}}"
}{
"method": "remote_tool_name",
"bodyMapping": {
"param1": "$param1",
"param2": "$param2"
}
}path is optional here: omit it and the call goes to the connector's base URL
(or <host>/mcp if the base URL has no path). Set it only to send this one tool
somewhere else — see MCP Bridge.
Shapes the API response before it reaches the AI client. Two reasons to use it: keeping fields that should never travel out of the model's context, and not paying for upstream noise in the context window.
The mapping is applied on the way out, so anything you drop never reaches the agent or the model provider behind it. Your audit log still records the full upstream response, so shaping the answer does not cost you the evidence of what the API actually returned.
{
"transform": {
"mode": "select",
"exclude": ["customer.iban", "customer.taxId"],
"select": { "order": "$.id", "total": "$.amounts.gross", "status": "$.state" }
},
"cacheTtl": 3600
}| Field | Description |
|---|---|
mode |
select (declarative template), jmespath (evaluate expression), or off to keep the config without applying it |
exclude |
Paths to drop, applied before everything else. The lever for PII and secrets |
include |
Keep only these paths, preserving the original document shape |
select |
Output template: keys are output names, leaves are paths ($.a.b[*].c) or literals |
expression |
JMESPath expression, for reshaping a template cannot express |
fallbackToRaw |
On error, return the raw response instead of failing. Default true |
maxBytes |
Hard cap on the serialized output. 0 or absent means no cap |
cacheTtl |
Seconds to cache the raw upstream response; shaping happens on read |
| Leaf | Result |
|---|---|
"$.a.b" / "a.b" |
Path lookup. The key is omitted when the path is not found |
"= [redacted]" |
The static string [redacted]. Leading = marks a literal, so a constant is not mistaken for a path |
42, true, null |
Passed through as-is |
{ "$from": "$.rows[*]", "$select": { … } } |
Iterate an array and reshape every element |
{ … } / [ … ] |
Nested template / array of the above |
Paths accept $.a.b, a[0].b, a[*].b and a['weird.key']. A leading $. is
optional. Depth, path count and output size are bounded, so a pathological
response or template cannot pin a worker.
Masking rather than dropping. exclude removes a field outright, which
changes the shape of the response. When an agent is better served by a stable
shape, replace the value instead:
{ "transform": { "mode": "select", "select": {
"order": "$.id",
"customer": "$.customer.name",
"iban": "= [redacted]"
} } }In the UI: the tool editor has a Response Mapping panel with the same three modes and a live preview that runs the mapping against a real response and reports the before/after size. A tool with a broken mapping is flagged in the tool list rather than failing silently.
A tool without a mapping behaves exactly as before — same object, byte for byte. The no-op path is the one that must never regress.
Annotations are the MCP spec's advisory hints that let an agent reason about a tool before calling it — above all whether it can change anything. An agent that knows a tool is read-only will probe it freely instead of asking for confirmation.
| Hint | Spec default | Meaning |
|---|---|---|
title |
— | Human-readable title for display |
readOnlyHint |
false |
The tool does not modify its environment |
destructiveHint |
true |
The write removes/overwrites rather than only adding. Meaningful only when readOnlyHint is false |
idempotentHint |
false |
Repeating the call with the same arguments changes nothing further. Meaningful only when readOnlyHint is false |
openWorldHint |
true |
Interacts with an external system of unbounded scope |
Annotations are hints, not access control. The spec states clients must never make trust decisions based on them. Real enforcement stays in roles and per-tool access.
You normally do not set these. AnythingMCP derives them from what the connector already declares:
| Connector | Signal | Result |
|---|---|---|
| REST | GET / HEAD / OPTIONS |
read-only |
| REST | POST |
write, additive, non-idempotent |
| REST | PUT / DELETE |
write, destructive, idempotent |
| REST | PATCH |
write, destructive, non-idempotent |
| GraphQL | query / mutation |
read-only / write |
| Database | connector readOnly flag, or SELECT vs INSERT/UPDATE/DELETE in the statement |
read-only / write; always openWorldHint: false |
| Any | static method (fixed text, no call) |
read-only |
| OData / REST | odata_* built-in methods |
read-only |
| SOAP | operation name only | never asserts read-only; flags a clearly-named destructive op |
| MCP bridge | the upstream server's own annotations | passed through verbatim |
An unambiguous tool name (delete_…, create_…) refines destructiveHint, but name heuristics never
assert readOnlyHint: wrongly claiming read-only would invite an agent to call a mutating tool freely,
whereas omitting the hint only makes it more careful.
Once the read/write verdict is known, all three of readOnlyHint, destructiveHint and idempotentHint
are emitted explicitly. A read-only tool gets destructiveHint: false and idempotentHint: true; a write
with nothing better known gets the spec defaults. Some directory reviewers (OpenAI's plugin portal, for
one) reject a tool whose destructiveHint is missing, even a read-only one, so nothing is left implicit.
The one case the derivation cannot solve is a read-only endpoint exposed over POST — very common for
search APIs, and indistinguishable from a write at the protocol level. Fix it per tool, in the UI via the
tool's Hints button, or over the API:
# Mark a POST-based search as read-only
curl -X PATCH .../api/connectors/<connectorId>/tools/<toolId>/annotations \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"annotations": {"readOnlyHint": true}}'
# Inspect derived vs override vs effective
curl .../api/connectors/<connectorId>/tools/<toolId>/annotations -H 'Authorization: Bearer <token>'
# Drop the override, go back to derived
curl -X PATCH .../api/connectors/<connectorId>/tools/<toolId>/annotations \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"annotations": null}'An override survives a re-import. Annotations coming from an upstream MCP server are refreshed on re-import, since that server is authoritative about its own tools.
A catalog adapter that knows better can ship the override itself, as an annotations object on the tool
("annotations": {"readOnlyHint": true}). It seeds the same per-tool override at install, so the operator
can still change it, and a catalog re-sync never overwrites it on a tool that already exists.
Connectors often front a service-based API while users authenticate to AnythingMCP individually (OAuth, per-user MCP API keys). The target system then only ever sees the service identity and cannot record who actually asked. These reserved variables forward the calling identity so it can — in headers, query parameters, the body, or the path:
{
"method": "POST",
"path": "/tickets",
"headers": {
"X-Requested-By": "{{amcp.user_email}}"
},
"bodyMapping": {
"subject": "$subject",
"requestedBy": "{{amcp.user_email}}"
}
}| Variable | Value |
|---|---|
{{amcp.user_email}} |
E-mail of the calling user |
{{amcp.user_id}} |
Internal user id |
{{amcp.org_id}} |
Workspace (organization) id |
{{amcp.server_id}} |
Id of the MCP server that exposed the tool |
{{amcp.server_name}} |
Name of that MCP server |
{{amcp.auth_method}} |
jwt, mcp_api_key, static_api_key, static_bearer or none |
{{amcp.api_key_name}} |
Name of the MCP API key used, when applicable |
Behaviour worth knowing:
- Opt-in. Nothing is forwarded unless you write the variable somewhere. Identity is personal data, so it is never attached automatically.
- Not always available. Identity exists for OAuth and per-user MCP API keys. With an instance-wide static
API key or bearer token, and in anonymous mode, there is no user — the variable resolves to an empty
string rather than leaking a literal
{{amcp.…}}to the target. - Non-spoofable. The values are resolved server-side from the authenticated request and merged after connector env vars, so neither a workspace variable nor a tool argument can shadow them.
- Typos are rejected when you save the tool, since at runtime an unknown reserved variable would silently resolve to empty.
- These variables apply to
baseUrl, connector headers and the whole endpoint mapping —path,queryParams,bodyMappingandbodyTemplate. In abodyTemplatethe substituted value is escaped for its JSON string context, so a quote or backslash cannot break the document. They are not substituted insideauthConfig. - The Test button in the connector UI does not run through an authenticated MCP session, so
{{amcp.*}}resolves to empty there; the test result says so. Call the tool from a connected MCP client to see the real value.
{
"name": "search_products",
"description": "Search products by keyword and category",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search keyword" },
"category": { "type": "string", "description": "Product category" },
"limit": { "type": "integer", "description": "Max results (default 10)" }
},
"required": ["query"]
},
"endpointMapping": {
"method": "GET",
"path": "/api/products/search",
"queryParams": {
"q": "$query",
"cat": "$category",
"limit": "$limit"
}
},
"responseMapping": {
"transform": {
"select": {
"total": "$.meta.totalCount",
"products": {
"$from": "$.data[*]",
"$select": {
"id": "id",
"name": "attributes.name",
"price": "attributes.price.amount",
"category": "relationships.category.name"
}
}
}
}
}
}responseMapping.transform shapes the response before it reaches the AI client —
fewer tokens, and only the fields this tool needs leave the workspace. It is
optional: without it the raw upstream response is returned unchanged. See
REST connector → Response Mapping for the
full syntax (select, include, exclude, JMESPath, maxBytes) — it applies
to every connector type, not just REST.
In addition to the standard NONE / API_KEY / BEARER_TOKEN / BASIC_AUTH / OAUTH2 / QUERY_AUTH / WS_SECURITY / CERTIFICATE / CONNECTION_STRING schemes, the connector spec supports LOGIN_TOKEN for APIs that issue a long-lived bearer in exchange for a credentials POST — optionally requiring the password to be bcrypt-hashed with a salt fetched from the remote service (Sorare-style).
The engine handles salt fetch → bcrypt → login → token cache → proactive refresh → re-login-on-401 automatically. Adapter authors declare the full flow as JSON:
{
"authType": "LOGIN_TOKEN",
"authConfig": {
"loginUrl": "https://api.example.com/graphql",
"loginMethod": "POST",
"loginBody": {
"query": "mutation Login($email: String!, $password: String!) { signIn(input: {email: $email, password: $password}) { jwtToken { token expiredAt } } }",
"variables": { "email": "${username}", "password": "${passwordHashed}" }
},
"username": "{{SERVICE_EMAIL}}",
"password": "{{SERVICE_PASSWORD}}",
"aud": "{{SERVICE_AUD}}",
"passwordHashing": {
"scheme": "bcrypt",
"saltSource": {
"type": "fetch",
"method": "GET",
"url": "https://api.example.com/api/v1/users/${username}",
"responsePath": "salt"
},
"outputParam": "passwordHashed"
},
"tokenJsonPath": "data.signIn.jwtToken.token",
"expiryJsonPath": "data.signIn.jwtToken.expiredAt",
"expiryFormat": "iso8601",
"tokenTTLSeconds": 2592000,
"refreshOn401": true,
"proactiveRefreshSeconds": 86400,
"headerName": "Authorization",
"headerTemplate": "Bearer ${token}",
"extraHeaders": { "JWT-AUD": "${aud}" }
}
}See docs/connectors/login-token-auth.md for the full field-by-field reference, salt-source types (fetch vs static), expiry formats (iso8601 / unix / ttl_seconds), and re-login policies.
Back to README | API Reference | REST Connector | LOGIN_TOKEN reference