Skip to content

feat: publish typed Agent Server OpenAPI contract - #4229

Merged
neubig merged 4 commits into
mainfrom
feat/oss-6121-agent-server-openapi-contract
Jul 27, 2026
Merged

neubig merged 4 commits into
mainfrom
feat/oss-6121-agent-server-openapi-contract

Conversation

@neubig

@neubig neubig commented Jul 27, 2026 •

Copy link
Copy Markdown
Member

HUMAN:

I have checked the schema and it looks good.


AGENT:

Why

The Python/Pydantic Agent Server models are authoritative, but consumers cannot currently obtain a deterministic, release-specific public OpenAPI document, and several MCP/settings output schemas degrade to empty or unrestricted values. That prevents @openhands/typescript-client from reproducibly generating the contract and allows security-sensitive settings shapes to drift unnoticed.

Fixes #4227
Linear: OSS-6121

Summary

  • Add one deterministic public OpenAPI exporter shared by make test-server-schema, release tooling, and the existing API compatibility checker.
  • Expose typed MCP transports, authentication/OAuth state, test request/response unions, name-keyed MCP settings, and sparse RFC 7386 patch shapes without changing runtime dict/secret serialization behavior.
  • Add an exact, owned weak-schema allowlist and recursive CI quality checker; publish version-matched openapi.json with release binaries and checksums.

REST API contract changes

Compared with base OpenAPI 14876fc97180 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -490 +490 @@
-response POST /api/mcp/test 200 application/json schema=anyOf=[MCPTestSuccess,MCPTestFailure]
+response POST /api/mcp/test 200 application/json schema=oneOf=[MCPTestSuccess,MCPTestFailure]
@@ -1679,0 +1680,4 @@
+schema MCPApiKeyAuthCredential-Output property header_name optional schema=anyOf=[type="string",type="null"]
+schema MCPApiKeyAuthCredential-Output property strategy required schema=type="string" const="api_key"
+schema MCPApiKeyAuthCredential-Output property value optional schema=anyOf=[type="string",type="null"]
+schema MCPApiKeyAuthCredential-Output type="object"
@@ -1683,0 +1688,4 @@
+schema MCPBasicAuthCredential-Output property password optional schema=anyOf=[type="string",type="null"]
+schema MCPBasicAuthCredential-Output property strategy required schema=type="string" const="basic"
+schema MCPBasicAuthCredential-Output property username required schema=type="string"
+schema MCPBasicAuthCredential-Output type="object"
@@ -1686,0 +1695,5 @@
+schema MCPBearerAuthCredential-Output property strategy required schema=type="string" const="bearer"
+schema MCPBearerAuthCredential-Output property value optional schema=anyOf=[type="string",type="null"]
+schema MCPBearerAuthCredential-Output type="object"
+schema MCPConfig type="object" additionalProperties=MCPServer-Output
+schema MCPConfigPatch type="object" additionalProperties=anyOf=[MCPServerPatch,type="null"]
@@ -1690,2 +1703,5 @@
-schema MCPNoneAuthCredential-Input property strategy required schema=type="string" const="none"
-schema MCPNoneAuthCredential-Input type="object"
+schema MCPHeaderAuthCredential-Output property headers optional schema=type="object" additionalProperties=anyOf=[type="string",type="null"]
+schema MCPHeaderAuthCredential-Output property strategy required schema=type="string" const="header"
+schema MCPHeaderAuthCredential-Output type="object"
+schema MCPNoneAuthCredential property strategy required schema=type="string" const="none"
+schema MCPNoneAuthCredential type="object"
@@ -1695,0 +1712,4 @@
+schema MCPOAuthAuthCredential-Output property authentication optional schema=anyOf=[MCPOAuthAuthentication-Output,type="null"]
+schema MCPOAuthAuthCredential-Output property state optional schema=anyOf=[MCPOAuthState-Output,type="null"]
+schema MCPOAuthAuthCredential-Output property strategy required schema=type="string" const="oauth2"
+schema MCPOAuthAuthCredential-Output type="object"
@@ -1704,0 +1725,9 @@
+schema MCPOAuthAuthentication-Output property additional_client_metadata optional schema=anyOf=[type="object" additionalProperties=true,type="null"]
+schema MCPOAuthAuthentication-Output property client_auth_method optional schema=anyOf=[type="string" enum=["none","client_secret_post","client_secret_basic","private_key_jwt"],type="null"]
+schema MCPOAuthAuthentication-Output property client_id optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthAuthentication-Output property client_metadata_url optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthAuthentication-Output property client_name optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthAuthentication-Output property client_secret optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthAuthentication-Output property scopes optional schema=anyOf=[type="string",type="array" items=type="string",type="null"]
+schema MCPOAuthAuthentication-Output property type required schema=type="string" const="oauth"
+schema MCPOAuthAuthentication-Output type="object" additionalProperties=false
@@ -1708,0 +1738,2 @@
+schema MCPOAuthClientInfoState-Output property client_secret optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthClientInfoState-Output type="object" additionalProperties=true
@@ -1719 +1750,8 @@
-schema MCPOAuthStateResponse {}
+schema MCPOAuthState-Output property client_info optional schema=anyOf=[MCPOAuthClientInfoState-Output,type="null"]
+schema MCPOAuthState-Output property token_expires_at optional schema=anyOf=[type="number",type="null"]
+schema MCPOAuthState-Output property tokens optional schema=anyOf=[MCPOAuthTokenState-Output,type="null"]
+schema MCPOAuthState-Output type="object"
+schema MCPOAuthStateResponse property client_info optional schema=anyOf=[type="object" additionalProperties=true,type="null"]
+schema MCPOAuthStateResponse property token_expires_at optional schema=anyOf=[type="number",type="null"]
+schema MCPOAuthStateResponse property tokens optional schema=anyOf=[type="object" additionalProperties=true,type="null"]
+schema MCPOAuthStateResponse type="object"
@@ -1733,0 +1772,3 @@
+schema MCPOAuthTokenState-Output property access_token optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthTokenState-Output property refresh_token optional schema=anyOf=[type="string",type="null"]
+schema MCPOAuthTokenState-Output type="object" additionalProperties=true
@@ -1735 +1776 @@
-schema MCPServer-Input property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential-Input,MCPApiKeyAuthCredential-Input,MCPBearerAuthCredential-Input,MCPBasicAuthCredential-Input,MCPHeaderAuthCredential-Input,MCPOAuthAuthCredential-Input],type="null"]
+schema MCPServer-Input property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential,MCPApiKeyAuthCredential-Input,MCPBearerAuthCredential-Input,MCPBasicAuthCredential-Input,MCPHeaderAuthCredential-Input,MCPOAuthAuthCredential-Input],type="null"]
@@ -1748 +1789,28 @@
-schema MCPServer-Output {}
+schema MCPServer-Output property args optional schema=anyOf=[type="array" items=type="string",type="null"]
+schema MCPServer-Output property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential,MCPApiKeyAuthCredential-Output,MCPBearerAuthCredential-Output,MCPBasicAuthCredential-Output,MCPHeaderAuthCredential-Output,MCPOAuthAuthCredential-Output],type="null"]
+schema MCPServer-Output property command optional schema=anyOf=[type="string" minLength=1,type="null"]
+schema MCPServer-Output property cwd optional schema=anyOf=[type="string",type="null"]
+schema MCPServer-Output property description optional schema=anyOf=[type="string",type="null"]
+schema MCPServer-Output property env optional schema=anyOf=[type="object" additionalProperties=anyOf=[type="string",type="null"],type="null"]
+schema MCPServer-Output property headers optional schema=anyOf=[type="object" additionalProperties=anyOf=[type="string",type="null"],type="null"]
+schema MCPServer-Output property icon optional schema=anyOf=[type="string",type="null"]
+schema MCPServer-Output property keep_alive optional schema=anyOf=[type="boolean",type="null"]
+schema MCPServer-Output property sse_read_timeout optional schema=anyOf=[type="number",type="null"]
+schema MCPServer-Output property timeout optional schema=anyOf=[type="number",type="null"]
+schema MCPServer-Output property transport optional schema=anyOf=[type="string" enum=["stdio","http","sse","streamable-http"],type="null"]
+schema MCPServer-Output property url optional schema=anyOf=[type="string" minLength=1,type="null"]
+schema MCPServer-Output type="object" additionalProperties=false
+schema MCPServerPatch property args optional schema=anyOf=[type="array" items=type="string",type="null"]
+schema MCPServerPatch property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential,MCPApiKeyAuthCredential-Input,MCPBearerAuthCredential-Input,MCPBasicAuthCredential-Input,MCPHeaderAuthCredential-Input,MCPOAuthAuthCredential-Input],type="null"]
+schema MCPServerPatch property command optional schema=anyOf=[type="string" minLength=1,type="null"]
+schema MCPServerPatch property cwd optional schema=anyOf=[type="string",type="null"]
+schema MCPServerPatch property description optional schema=anyOf=[type="string",type="null"]
+schema MCPServerPatch property env optional schema=anyOf=[type="object" additionalProperties=anyOf=[type="string" format="password",type="null"],type="null"]
+schema MCPServerPatch property headers optional schema=anyOf=[type="object" additionalProperties=anyOf=[type="string" format="password",type="null"],type="null"]
+schema MCPServerPatch property icon optional schema=anyOf=[type="string",type="null"]
+schema MCPServerPatch property keep_alive optional schema=anyOf=[type="boolean",type="null"]
+schema MCPServerPatch property sse_read_timeout optional schema=anyOf=[type="number",type="null"]
+schema MCPServerPatch property timeout optional schema=anyOf=[type="number",type="null"]
+schema MCPServerPatch property transport optional schema=anyOf=[type="string" enum=["stdio","http","sse","streamable-http"],type="null"]
+schema MCPServerPatch property url optional schema=anyOf=[type="string" minLength=1,type="null"]
+schema MCPServerPatch type="object" additionalProperties=false
@@ -2569 +2637 @@
-schema _RemoteMCPServerSpec property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential-Input,MCPApiKeyAuthCredential-Input,MCPBearerAuthCredential-Input,MCPBasicAuthCredential-Input,MCPHeaderAuthCredential-Input,MCPOAuthAuthCredential-Input],type="null"]
+schema _RemoteMCPServerSpec property auth optional schema=anyOf=[oneOf=[MCPNoneAuthCredential,MCPApiKeyAuthCredential-Input,MCPBearerAuthCredential-Input,MCPBasicAuthCredential-Input,MCPHeaderAuthCredential-Input,MCPOAuthAuthCredential-Input],type="null"]

Issue Number

#4227 / OSS-6121

How to Test

SDK validation performed on commit da2876f6:

  • make test-server-schema — exported twice byte-identically, passed the type-quality ratchet with 98 explicitly allowlisted current locations, and passed Swagger validation.
  • Focused contract/router/compatibility suite — 146 passed.
  • Contract/settings/runtime suite after the final static-typing adjustment — 205 passed.
  • Commit-time pre-commit hooks — YAML, Ruff format/lint, pycodestyle, Pyright, import rules, and tool registration all passed for every staged file.
  • git diff --check passed.

Canvas compatibility was exercised against this exact local SDK checkout, not a released server:

  1. Built the current OpenHands/agent-canvas production app.
  2. Started its real Agent Server + automation + ingress stack with OH_AGENT_SERVER_LOCAL_PATH pointing at this branch.
  3. Ran tests/e2e/mock-llm/mcp/mock-llm-mcp-github.spec.ts in Chromium.
  4. All 4 tests passed: marketplace rendering, install form, credential-bearing install/persistence, and deletion.

The Canvas run used a temporary HOME plus separate XDG config/data/cache paths, a workspace-only OH_CANVAS_SAFE_STATE_DIR, isolated settings/persistence/session keys, and non-default service ports. It never read or wrote ~/.openhands; generated state and services were removed after the run.

Video/Screenshots

Not applicable: this PR changes the server contract/export and release artifacts without changing Canvas UI. The real Canvas full-stack E2E result above is the behavioral evidence.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

The public release artifact prunes unreachable components, while the existing release-to-release checker deliberately retains historical components so it can enforce deprecation runway after a route is removed.

A separate pre-commit run pyright --all-files diagnostic run was killed with exit 247 after reporting zero errors in the changed code and four existing warnings in untouched files. The required staged-file Pyright hook passed cleanly during commit.


Agent Server images for this PR

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:86b6cbb-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-86b6cbb-python \
  ghcr.io/openhands/agent-server:86b6cbb-python

All tags pushed for this build

ghcr.io/openhands/agent-server:86b6cbb-golang-amd64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-golang-amd64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-golang-amd64
ghcr.io/openhands/agent-server:86b6cbb-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:86b6cbb-golang-arm64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-golang-arm64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-golang-arm64
ghcr.io/openhands/agent-server:86b6cbb-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:86b6cbb-java-amd64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-java-amd64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-java-amd64
ghcr.io/openhands/agent-server:86b6cbb-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:86b6cbb-java-arm64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-java-arm64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-java-arm64
ghcr.io/openhands/agent-server:86b6cbb-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:86b6cbb-python-amd64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-python-amd64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-python-amd64
ghcr.io/openhands/agent-server:86b6cbb-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:86b6cbb-python-arm64
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-python-arm64
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-python-arm64
ghcr.io/openhands/agent-server:86b6cbb-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:86b6cbb-golang
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-golang
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-golang
ghcr.io/openhands/agent-server:86b6cbb-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:86b6cbb-java
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-java
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-java
ghcr.io/openhands/agent-server:86b6cbb-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:86b6cbb-python
ghcr.io/openhands/agent-server:86b6cbb62588c552b9a3ba7f6a786fb6eddd30fa-python
ghcr.io/openhands/agent-server:feat-oss-6121-agent-server-openapi-contract-python
ghcr.io/openhands/agent-server:86b6cbb-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 86b6cbb-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 86b6cbb-python-amd64) are also available if needed

@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   api.py3102791%133, 135–140, 142, 144, 146, 188, 200, 215, 221, 277, 282, 291–293, 323, 329, 333, 354–355, 601, 604, 610
   mcp_router.py3235583%121, 207, 210, 216, 221, 389–390, 399, 434–436, 439–440, 443, 474–477, 480–481, 485–486, 514, 516–517, 525, 553–554, 622–623, 635, 638, 643, 727, 756–757, 764, 783–784, 789, 791–794, 796–798, 811–815, 825–827
   openapi.py87594%32, 51, 54, 58, 64
openhands-sdk/openhands/sdk/mcp
   config.py3737281%93, 98–108, 122–124, 154, 180, 204, 326, 346, 388–391, 395–398, 402–404, 430–433, 437, 451, 455, 459, 464, 534, 536, 542, 554, 559–562, 568–570, 576, 613, 655–659, 661, 663–665, 667–670, 672–674, 681, 684, 686
openhands-sdk/openhands/sdk/settings
   api_models.py71494%112, 122, 181, 183
TOTAL39049754281% 

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
neubig requested a review from hieptl July 27, 2026 18:38

@hieptl hieptl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! 🙏

@neubig
neubig merged commit 300c92e into main Jul 27, 2026
39 checks passed
@neubig
neubig deleted the feat/oss-6121-agent-server-openapi-contract branch July 27, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Agent Server] Publish a deterministic, type-quality-checked OpenAPI contract

2 participants