# K Protocol Test Service
Standalone public interoperability service targeting `https://ktest.kai.com`. It implements the repository's K Protocol V1.1 draft with MCP `2026-07-28`, HTTPS Streamable HTTP and MCP Apps `2026-01-26`. Building or testing locally does not deploy the public domain.
All credentials and accounts below are public synthetic fixtures. Workspaces expire after 30 minutes, access tokens after five minutes, refresh grants after one hour, authorization codes after one minute and consent requests after ten minutes. Restarting clears all state. Do not submit private information. Each scenario has its own MCP registry and workspace store; OAuth accounts further partition workspace state. Anonymous and configured-header scenarios share synthetic data within a scenario, so choose a unique workspace ID.
## Discover an App
Use the domain, `/`, `/.well-known/ai-catalog.json`, an individual `/apps/<id>` introduction, `/apps/<id>/catalog.json` or `/apps/<id>/server-card.json`. Introduction pages publish both HTML and HTTP Link relations. A QR code must encode the same ordinary HTTPS introduction URL; there is no special QR protocol. Cards include icons, usage examples and live endpoint/version declarations. The API Key Card also includes an alternative remote with URL and secret-header variables for connection-configuration tests. Cards and Catalogs support ETag and Last-Modified conditional requests.
`/scenarios.json` provides a machine-readable matrix. `/healthz` and `/readyz` return status without running business tools. A host still performs its own add, authorize, permission and removal flows; opening a discovery document never executes a tool.
| App ID | MCP path | Credentials |
| -------------------------- | ------------------------------- | -------------------------------------------------------- |
| `anonymous` | `/mcp/anonymous` | None |
| `api-key` | `/mcp/api-key` | `X-API-Key: ktest-api-key` |
| `bearer` | `/mcp/bearer` | `Authorization: Bearer ktest-bearer-token` |
| `basic` | `/mcp/basic` | Basic username `ktest`, password `ktest-password` |
| `oauth-cimd` | `/mcp/oauth-cimd` | Client ID Metadata Document and public PKCE |
| `oauth-dcr` | `/mcp/oauth-dcr` | Dynamic client registration, public or confidential PKCE |
| `oauth-preregistered` | `/mcp/oauth-preregistered` | Pre-registered public or confidential PKCE |
| `oauth-client-credentials` | `/mcp/oauth-client-credentials` | Confidential machine client |
Configured API Key, Basic and static Bearer headers are compatibility exercises. Standard MCP account authorization uses the OAuth scenarios. Historical MCP versions and the old HTTP+SSE transport are outside K Protocol V1.1.
## OAuth flows
Each OAuth mode has a separate issuer at `/oauth/<mode>`. Its metadata is available at `/.well-known/oauth-authorization-server/oauth/<mode>`, `/.well-known/openid-configuration/oauth/<mode>` and `/oauth/<mode>/.well-known/openid-configuration`. The OIDC discovery aliases describe OAuth endpoints; this fixture does not issue OIDC ID tokens.
Protected Resource Metadata is at `/.well-known/oauth-protected-resource/mcp/oauth-<mode>` and is referenced by 401/403 challenges. The root PRM points to the CIMD scenario. Token and authorization requests must include the exact resource, such as `https://ktest.kai.com/mcp/oauth-cimd`. Tokens cannot cross scenarios.
| Client ID | Authentication | Registered callbacks |
| ------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
| `https://ktest.kai.com/clients/public.json` | `none`, CIMD | `https://ktest.kai.com/callback` |
| `ktest-public` | `none` | Public `/callback`, `http://127.0.0.1:8765/callback`, `http://localhost:8765/callback` |
| `ktest-confidential-basic` | `client_secret_basic` | Public `/callback` |
| `ktest-confidential-post` | `client_secret_post` | Public `/callback` |
Both confidential clients use the public secret `ktest-client-secret`. Client ID and secret in Basic authentication use the OAuth form-encoding convention. For arbitrary callbacks, use a valid public CIMD document or dynamic registration. CIMD metadata is fetched through the K public network policy, which rejects private, loopback and cloud metadata destinations. Dynamic registration is available only at `/oauth/dcr/register`; it returns an expiring client ID and a secret when requested. Supported grants are `authorization_code` and `refresh_token`; machine credentials have their own scenario.
Start authorization at `/oauth/<mode>/authorize` with `response_type=code`, the registered `client_id` and exact `redirect_uri`, `resource`, `scope`, `state`, an S256 `code_challenge` and `code_challenge_method=S256`. The browser form lets you allow or deny access as Alice or Bob. Its callback preserves `state` and identifies `iss`. Codes bind the account, client, redirect, scopes, issuer and resource and can be exchanged once at `/oauth/<mode>/token` with the PKCE verifier. No login to a real service is involved.
`test:read` permits discovery and reads. `test:write` adds `write_note` and `delete_note`; request both for writes. Read-only lists hide write tools, and direct write attempts receive 403 with the needed scope. Missing, invalid, expired, revoked and wrong-resource tokens receive 401. Refresh rotates the refresh token and cannot expand scopes. `/oauth/<mode>/revoke` revokes the token family, including prior access tokens. Unknown tokens do not reveal grant state.
Example machine token request:
```sh
curl https://ktest.kai.com/oauth/client-credentials/token \
--data-urlencode grant_type=client_credentials \
--data-urlencode client_id=ktest-confidential-post \
--data-urlencode client_secret=ktest-client-secret \
--data-urlencode resource=https://ktest.kai.com/mcp/oauth-client-credentials \
--data-urlencode scope='test:read test:write'
```
This fixture uses opaque tokens. JWT assertion grants, DPoP, mTLS, external enterprise identity, payment/subscription checks and identity-provider federation are not requirements of the selected K Protocol draft and are not advertised.
## MCP exercises
`server/discover` reports live capabilities. The service uses standard MCP messages, metadata and errors without K-specific RPC envelopes.
| Tool | Exercise |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `echo`, `add` | Validated inputs, output Schemas, meaningful text and structured results |
| `identity` | Current synthetic subject, scenario and negotiated client capabilities; no credentials returned |
| `read_workspace` | Query a temporary workspace and open its App interface |
| `write_note` | Create a note with `workspace`, `text`, `idempotencyKey`; identical retries return the original receipt and conflicting keys fail |
| `delete_note` | Delete using `workspace`, `id`, `expectedRevision`; stale revisions fail |
| `ui_read_workspace` | App-only tool metadata; the host enforces visibility and the service still checks account permissions |
| `test_error` | Deliberate `isError` business failure |
| `wait` | Bounded delay, progress and logs; abort its HTTP request to exercise cancellation |
| `notify` | Publish list-change notifications for subscription tests |
| `request_form`, `request_url` | Form and URL elicitation through standard keyed input continuation |
| `request_sampling`, `request_roots` | Client sampling and root-list input through standard continuation |
Input-required tools take `{ "text": "A synthetic request" }`. Declare the relevant client capabilities. Preserve the returned opaque `requestState` and return exactly the keyed `input` response through standard `inputResponses`. Continuations expire after five minutes and bind the scenario, account, tool and original text. Unknown, reused or mismatched continuations fail. Form input requires a nonempty `label`; declining/cancelling returns its standard result. No input-required tool writes business data. The URL page is a synthetic acknowledgement, with acceptance controlled by the host.
Read `ktest://about`, `ktest://binary`, `ktest://workspace/<workspace>` or `ktest://fixture/0` through `ktest://fixture/109`. The 113 listed resources exercise cursor pagination. Workspace templates support completion. `summarize_workspace` supports workspace/style arguments and completion. Unknown tools/resources, malformed input and unsupported methods use standard MCP errors. MCP subscriptions deliver resource updates after writes and list changes from `notify`. Request metadata controls progress and logging; no background heartbeat is required.
`ui://ktest/workspace.html` has the MCP Apps MIME type and standard UI metadata, with no external asset or connection domains. Its official SDK client exercises initialization, existing-result rendering, host context, app-only and ordinary tool calls, resource reads, model-context updates, user messages, external links, fullscreen/inline requests, size changes and teardown. Actions require explicit controls. Loading a result never replays its write. Hosts without UI support can use the text and structured results.
## Discovery and indexing fixtures
Use `/fixtures/<id>/server-card.json`:
| ID | Expected observation |
| --------------- | -------------------------------------------------------------- |
| `noindex` | Publicly declared Card with `X-Robots-Tag: noindex` |
| `robots-denied` | Publicly declared Card disallowed by `/robots.txt` |
| `unlisted` | Valid Card absent from the publisher's Catalog |
| `withdrawn` | 410 Gone; an existing Index record can be withdrawn on refresh |
| `malformed` | Invalid JSON with Card media type |
| `incompatible` | Card advertising an unsupported protocol/transport |
| `redirect` | Redirect to the anonymous Card |
| `unavailable` | 503 with Retry-After |
Public indexing must use anonymous publication evidence and preserve the difference between exclusion, withdrawal and unconfirmed discovery. Private grants, tools, results and workspaces are never added to the Catalog.
## Run and verify
From the repository root:
```sh
devenv shell -- pnpm --filter @kaicom/k build
devenv shell -- pnpm --filter @kaicom/ktest build
devenv shell -- pnpm --filter @kaicom/ktest test
devenv shell -- pnpm --filter @kaicom/ktest start
```
| Setting | Default | Purpose |
| --------------------------------- | ----------------------- | ------------------------------------------- |
| `KTEST_PUBLIC_ORIGIN` | `https://ktest.kai.com` | Published exact HTTPS origin |
| `KTEST_HOST` | `0.0.0.0` | Listener address |
| `KTEST_PORT` | `3001` | Listener port |
| `KTEST_ALLOWED_ORIGINS` | Public origin | Comma-separated exact HTTPS browser origins |
| `KTEST_TLS_KEY`, `KTEST_TLS_CERT` | Unset | Supply both to serve local HTTPS directly |
Normally the listener uses HTTP behind a TLS reverse proxy. A local test can set `KTEST_PUBLIC_ORIGIN=https://localhost:7445`, the port to `7445`, and certificate/key paths, then trust the development certificate in the host. Server-side clients need no browser Origin header. Browser clients require their exact origin in `KTEST_ALLOWED_ORIGINS`; preflight exposes MCP and authorization headers.
Build the standalone image with `devenv shell -- docker build -f docker/ktest.Dockerfile -t kaicom/ktest .`, then run `devenv shell -- docker compose -f compose.ktest.yaml up -d --build`. The Compose service binds HTTP only to loopback. Install `docker/ktest.nginx.conf` in the public reverse proxy with the domain's TLS certificates. It disables response buffering and caching for MCP streams. Check `/healthz`, the Catalog and `server/discover` through the actual HTTPS hostname after provisioning DNS and TLS. Run a single replica: synthetic state is intentionally process-local. Rolling replacement invalidates test grants and continuations.
### CI/CD
GitLab CI includes ktest in the existing `check`, `images` and `deploy` stages. Every branch validates its AMD64 image by starting the packaged service and checking health, the public Catalog identity and anonymous MCP discovery. Default-branch pushes and release tags publish `$CI_REGISTRY_IMAGE/ktest:<commit-sha>` plus `:latest` or the version tag, using the same release identity as Platform and Web.
A default-branch push deploys all three verified images through the existing SSH archive and [production Compose](../compose.production.yaml). K Test runs without Platform credentials or a database, binds only `127.0.0.1:3001`, and is exposed by the streaming Nginx virtual server at `https://ktest.kai.com`. Prepare that domain's DNS and TLS certificate on the deployment host before the first release. `KTEST_ALLOWED_ORIGINS` in the host environment can list permitted browser origins, separated by commas; its default is `https://ktest.kai.com`. Replacing the container clears its temporary synthetic state.