Harnix
Build

Build an MCP server

The standards, keywords and authentication handling an MCP server you write needs so it plugs into Harnix as a connector.

This page is for teams who write their own MCP server (an internal API, an order system, a CRM…) so agents on Harnix can call it through a connector. Harnix is a remote MCP client: it calls your server over HTTPS on behalf of the organization that added the connector.

What your server needs, in short:

AreaStandard
ProtocolModel Context Protocol, using an official SDK (Python, TypeScript…)
TransportStreamable HTTP at a single URL (e.g. https://mcp.example.com/mcp); legacy SSE is still supported. stdio is not
Networkhttps, a public host name, no redirect on the MCP URL
AuthenticationOne of four: none, a fixed header, OAuth (authorization code + PKCE), OAuth client credentials
Toolssnake_case names, a clear description, a JSON Schema inputSchema, readOnlyHint: true on read-only tools
Resultstext content, at most 64 KB, isError: true for business errors

Connection requirements

  • URL. One MCP endpoint, e.g. https://mcp.example.com/mcp. It must be https and resolve to a public address; private/internal addresses (10.x, 192.168.x, localhost…) and URLs with a username or password are refused. Harnix does not follow redirects: a 3xx on the MCP URL is an error.
  • Transport. With Automatic, Harnix tries Streamable HTTP first, then SSE. New servers should use Streamable HTTP.
  • Timing. The handshake (initialize) and the tool list (tools/list) must finish within 10 seconds. A single tool call may run for up to 120 seconds.
  • Protocol version. Harnix initializes with protocolVersion 2025-06-18; the official SDKs negotiate versions for you.
  • The tool list is read again whenever a run starts. A tool you add to the server is enabled for every agent using the connector (an admin can switch individual tools off in the console).

Pick an authentication method

Method (in the console)Pick it whenYour server must
No authThe data is public, or the server only accepts trusted IP rangesNothing
HeadersAn internal API with one key for the whole organization. The fastest route for internal teamsCheck a fixed header
Client credentialsServer-to-server through an existing identity provider (Keycloak, Auth0, Entra ID…), nobody signs inBe an OAuth resource server: discovery + access-token checks
Automatic → OAuthTools must act with a user account's permissions (Google Drive, a per-user CRM…)Be a full OAuth resource server per MCP Authorization

A header-protected server needs Headers, not Automatic

With Automatic, Harnix sends initialize with nothing attached; if the server answers 401, Harnix takes that to mean OAuth and looks for the discovery documents. A server that only checks an API key has none, and the connector reports “Couldn't read this server's connection settings.” (discovery_failed).

Fixed header

The admin enters a header name and value in the connector sheet, e.g. Authorization: Bearer <key> or X-API-Key: <key>. Harnix sends that header on every request to the MCP URL. The value is encrypted at rest and never shown again.

On your server:

  • Refuse a request with a missing or wrong header with 401 (or 403).
  • Compare keys in constant time (hmac.compare_digest, crypto.timingSafeEqual) and never log the header value.
  • To rotate: accept both the old and the new key for a while, update the header in the console, then drop the old key.

Every conversation through this connector uses the same key — if departments need different access, create several connectors with different keys.

OAuth: Harnix signs in on a user's behalf

This is the MCP Authorization flow: OAuth 2.1, authorization code + PKCE. Your MCP server is the resource server; tokens are issued by an authorization server (the same server, or your Keycloak/Auth0/Entra ID…).

Standards Harnix uses:

StandardPurposeRequired?
RFC 9728 — OAuth Protected Resource MetadataThe MCP server names its authorization serverYes
RFC 8414 — Authorization Server Metadata (or OpenID Connect Discovery)Harnix reads endpoints and supported methodsYes
RFC 7636 — PKCE, method S256Protects the codeYes
RFC 8707 — Resource Indicators (resource)Tokens are issued for your MCP server specificallyYes (Harnix always sends it)
RFC 7591 — Dynamic Client RegistrationHarnix registers its own clientNo — without it the admin enters a Client ID
RFC 7009 — Token RevocationTokens are revoked when an admin clicks DisconnectNo
RFC 9207 — iss on the redirectGuards against an authorization-server mix-upNo — checked when present

Step 1 — Answer 401 with WWW-Authenticate

A request to the MCP URL with no token (or a wrong or expired one) gets:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="orders.read"
  • resource_metadata — the URL of the Protected Resource Metadata document. Without it, Harnix tries https://mcp.example.com/.well-known/oauth-protected-resource/mcp (after the MCP URL's path), then /.well-known/oauth-protected-resource.
  • scope (optional) — the scope to ask for.

Step 2 — Protected Resource Metadata

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["orders.read", "orders.write"]
}
  • resource is the MCP URL itself, as the admin enters it in Harnix.
  • Harnix uses the first entry of authorization_servers.

Step 3 — Authorization Server Metadata

Harnix reads https://auth.example.com/.well-known/oauth-authorization-server (with the issuer's path appended, if any), then /.well-known/openid-configuration.

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "registration_endpoint": "https://auth.example.com/register",
  "revocation_endpoint": "https://auth.example.com/revoke",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
  "grant_types_supported": ["authorization_code", "refresh_token"]
}
  • issuer must match the authorization server URL from step 2.
  • code_challenge_methods_supported must include S256, or the connector reports pkce_unsupported.
  • Every endpoint must be public https — the same rule as the MCP URL.
  • Harnix caches this document for up to 24 hours.

Step 4 — Client registration

With a registration_endpoint, Harnix registers itself (RFC 7591) with

{
  "client_name": "Harnix",
  "redirect_uris": ["https://app.harnix.vn/connectors/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

token_endpoint_auth_method is none (a public client) when the server supports it, client_secret_basic otherwise.

Without a registration_endpoint: create a client on the authorization server with redirect URI https://app.harnix.vn/connectors/callback and the authorization_code + refresh_token grants, then give the admin the OAuth Client ID (and the OAuth Client Secret for a confidential client) to enter under Advanced. With neither, the connector reports registration_unsupported.

Step 5 — Sign-in and code exchange

The admin clicks Connect; Harnix opens the authorization_endpoint with:

ParameterValue
response_typecode
client_idthe registered client
redirect_urihttps://app.harnix.vn/connectors/callback
statea random string, returned unchanged
code_challenge, code_challenge_methodPKCE, S256
resourcethe MCP URL
scopethe admin's configured scopes; otherwise the step 1 scope; otherwise every scopes_supported

Once the user approves, redirect back to redirect_uri with code and state (plus iss if you support RFC 9207). A sign-in attempt is good for 10 minutes. Harnix then calls the token_endpoint (application/x-www-form-urlencoded):

POST /token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)   ← only with a secret

grant_type=authorization_code&code=…&redirect_uri=https://app.harnix.vn/connectors/callback
&code_verifier=…&resource=https://mcp.example.com/mcp&client_id=…

With a secret, Harnix uses client_secret_basic when the metadata lists it (or says nothing), client_secret_post otherwise. A public client sends only client_id.

Token response:

{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "orders.read"
}

Return expires_in and a refresh_token. Without a refresh token, the admin must click Reconnect whenever the token expires.

Step 6 — Check the token on every MCP request

Harnix sends Authorization: Bearer <access_token> on every request. Your MCP server:

  1. Validates the token (JWT signature against the JWKS, or token introspection).
  2. Checks expiry.
  3. Checks the audience: the token must have been issued for your own MCP URL (the resource value). Never accept a token issued for another service.
  4. Checks scopes per tool.
SituationAnswerWhat Harnix does
Missing, wrong or expired token401 + WWW-Authenticate as in step 1Refreshes once and retries; still 401 → Needs reconnect
Valid token, not enough permission403 + WWW-Authenticate: Bearer error="insufficient_scope", scope="orders.write"Needs reconnect; the next sign-in asks for that scope too

Refresh and revocation

  • Harnix refreshes 60 seconds before the token expires (grant_type=refresh_token, with resource), or right after a 401.
  • Refresh-token rotation (a new refresh token on every refresh) is supported; Harnix never uses one refresh token twice in parallel.
  • A 4xx from the authorization server on refresh (e.g. invalid_grant) moves the connector to Needs reconnect. A 5xx or 429 is treated as transient; Harnix tries again on the next run.
  • When an admin clicks Disconnect, Harnix calls the revocation_endpoint (if any) for the refresh token, then the access token.

Client credentials

For when nobody signs in: Harnix fetches tokens itself with the Client ID and Client secret the admin enters in the console. Your server still does steps 1–3 above (401 + Protected Resource Metadata + Authorization Server Metadata, and the metadata must still list S256) and step 6 (token checks). Harnix calls:

POST /token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=client_credentials&resource=https://mcp.example.com/mcp&scope=orders.read&client_id=…

There is no refresh token: when the token expires or gets a 401, Harnix asks for a new one. A 4xx from the authorization server puts the connector in Error (client_credentials_rejected) — the admin checks the Client ID and secret.

Designing tools

The model reads a tool's name, description and schema to decide which tool to call with which arguments. A well-written tool matters as much as code that runs correctly.

  • Name: snake_case, ASCII, verb + noun (get_order_status, create_ticket). The model sees mcp_<connector name>_<tool name>, cut at 64 characters — keep tool names short.

  • description: what the tool does, when to use it, its limits. Write it for the model, not for a person.

  • inputSchema: a complete JSON Schema — types, required, enum, a description per field. Validate arguments on the server; never trust the model.

  • Annotations — decide whether the end-user is asked before the tool runs (see Confirmation before a tool call):

    AnnotationHarnix
    readOnlyHint: trueRuns right away
    Absent, or readOnlyHint: falseAsks the end-user first

    Only mark readOnlyHint: true when the tool truly changes nothing.

  • title (optional): a readable name, shown in the console.

Returning results

  • Return text content. Harnix reads only text blocks and joins them; images and binary resources are ignored. Return structured data as JSON text.
  • A result over 64 KB is cut, with a note. Page or summarize instead of returning a whole table.
  • Business errors (“order A-1003 not found”) → a result with isError: true and one explanatory sentence; the model reads it and answers the user. Don't throw for business errors.
  • Never put secrets, tokens or stack traces in a result: it goes straight into the prompt and shows up on the trace.
  • Idempotency: if a run is interrupted while a tool is running, Harnix may call that tool once more. Tools that write should guard against duplicates (e.g. take a client-generated order id, or check before creating).

Examples

# pip install "mcp[cli]"
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations

mcp = FastMCP("orders", stateless_http=True)

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_order_status(order_id: str) -> str:
    """Delivery status of one order. Use when a customer asks where an order is."""
    return f"Order {order_id} was handed to DHL on 2026-10-10."

@mcp.tool()  # no readOnlyHint → Harnix asks the end-user first
def create_order(sku: str, quantity: int) -> str:
    """Place a new order for a SKU."""
    return f"Order placed: {quantity} x {sku}."

if __name__ == "__main__":
    mcp.run(transport="streamable-http")  # served at /mcp

The OAuth side (steps 1–6) is usually handled by an existing authorization server; the official SDKs ship token verification and Protected Resource Metadata for resource servers — see your SDK's documentation.

Check before you hand it over

  1. Run the MCP Inspector (npx @modelcontextprotocol/inspector) against your URL: it connects, lists every tool, and each tool runs.

  2. For OAuth, check discovery with curl:

    curl -i -X POST https://mcp.example.com/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'
    # → 401 with resource_metadata in WWW-Authenticate
    curl https://mcp.example.com/.well-known/oauth-protected-resource/mcp
    curl https://auth.example.com/.well-known/oauth-authorization-server
    # → code_challenge_methods_supported includes "S256"
  3. Add the connector in the console with the right Authentication, click Test: you should see Found N tools.

  4. Assign it to a test agent, ask a question that needs a tool, and open Runs & traces to see the arguments and the result.

Error codes and server-side fixes

Code (the console shows a matching sentence)Usual causeFix
discovery_failedNo Protected Resource Metadata or Authorization Server Metadata; issuer mismatch; a header-protected server with the connector left on AutomaticImplement steps 1–3, or pick Headers
pkce_unsupportedThe metadata doesn't list S256Enable PKCE S256 on the authorization server
registration_unsupportedNo registration_endpoint and no Client ID enteredEnable Dynamic Client Registration, or create a client by hand and enter it under Advanced
token_exchange_failedThe token endpoint refused the code — wrong redirect_uri, code_verifier, or client authCheck the redirect URI https://app.harnix.vn/connectors/callback and the client auth method
refresh_rejectedThe refresh token was revoked or expired, or the server still answers 401 after a refreshCheck token audience and lifetime; the admin clicks Reconnect
insufficient_scopeThe server answered 403 insufficient_scopeWorking as intended: the admin Reconnects to grant the extra scope
client_credentials_rejectedWrong Client ID/secret, or a freshly issued token still gets 401Check the client and the token's audience
connection timed outNo answer within 10 seconds (connect) / 120 seconds (tool call)Speed up initialize/tools/list, check the firewall
An exception class name (e.g. ConnectError, ProbeStatusError)Unreachable, or an unexpected HTTP status (3xx, 5xx)Check the URL, the TLS certificate, redirects

Harnix never shows your server's own error text (it might contain a secret) — only the codes above or an exception class name. For details, read your server's logs.

On this page