Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions CHANGELOG.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

## [Unreleased]

콘솔 SSE 스트림의 wire 계약에 하나가 추가됐다(아래). 소비자가 컴파일하는 Java API 자체는
`0.0.1` 에서 바뀌지 않았고 그대로 링크된다. 그 밖에 바뀐 것은 내장 MCP 툴이 **무엇을 답하는가**다.
wire 계약 변경은 둘이다 — 콘솔 SSE 스트림에 하나가 추가됐고, **MCP 툴 오류의 problem detail 이
`structuredContent` 에서 `_meta` 로 옮겨졌다**(둘 다 아래). 소비자가 컴파일하는 Java API 는 상수
하나가 늘었을 뿐 `0.0.1` 에서 바뀌지 않았고 그대로 링크된다. 그 밖에 바뀐 것은 내장 MCP 툴이 **무엇을 답하는가**다.
이 툴들을 wrap·subclass·위임해서 쓰고 있다면 이 절 끝의 마이그레이션 노트를 읽어야 한다.

### 추가
Expand All @@ -27,9 +28,18 @@
연결 시점의 `capacity`. `EventSource` 가 재연결할 때마다 다시 나가며, 클라이언트는 모르는
필드를 무시해야 한다. 기존 클라이언트에는 영향이 없다 — `EventSource` 는 리스너가 없는 named
이벤트를 버린다. 문서는 `docs/guide/04-rest-api.ko.md`.
- `McpToolResult.PROBLEM_META_KEY`(`"org.htcom.protean/problem"`) — 코드가 붙은 툴 오류가 이제
problem detail 을 싣는 `_meta` 키(아래 *변경* 참고).

### 수정

- `protean.list_runtimes` 가 `runtimes[].scope` 를 `string` 으로 선언했는데, 같은 툴의 설명은
auto-provision 이 꺼져 있으면(기본값) `null` 이라고 적고 있었다. 그래서 기본 설치에서는 모든
호출이 툴의 `outputSchema` 를 어기는 결과를 냈다 — 검증하는 클라이언트는 `scope: null found,
string expected` 를 보고했고, `protean.mcp.strict-schema=true` 에서는 디스패처의 출력 검사가
호출을 `OUTPUT_SCHEMA_VIOLATION` 으로 바꿔 툴을 아예 쓸 수 없었다. 이제 `scope` 는
`["string", "null"]` 로 선언한다.

- `protean.list_modules` 의 `trustTier` 필터가 인자(`String`)를 `ModuleStatus.trustTier()`
(`TrustTier` enum)와 비교해 `String.equals(Object)` 가 모든 모듈에 대해 false 였다 —
**어떤 값을 줘도 필터가 아무것도 맞추지 못했다.** 에러도 없고 형태가 멀쩡한 빈 목록이
Expand All @@ -55,6 +65,18 @@

### 변경

- **코드가 붙은 MCP 툴 오류가 RFC 9457 problem detail 을 `structuredContent` 가 아니라 result
`_meta` 의 `org.htcom.protean/problem` 에 싣는다.** `0.0.1` 에서는 problem 객체(`type`·`title`·
`detail`·`code` 와 확장 멤버)를 `structuredContent` 에 썼는데, 스펙은 그 자리를 툴의
`outputSchema` 가 기술하는 페이로드에 두고 스키마를 따르도록 요구한다. `outputSchema` 를 선언한
내장 툴이 전부 해당됐다 — 예: auto-provision 이 꺼진 상태의 `protean.scope_list`, 없는 모듈에
대한 `protean.patch_module`. 오류 result 에서도 `structuredContent` 를 검증하는 클라이언트
(TypeScript SDK `@modelcontextprotocol/sdk` 1.x 가 그렇다)는 실제 오류를 `Structured content
does not match the tool's output schema` 로 바꿔 버려, 에이전트가 무엇이 잘못됐는지 보지
못했다. **오류에서 `structuredContent.code` 를 읽던 클라이언트는
`_meta["org.htcom.protean/problem"].code` 를 읽어야 한다.** `[CODE]` 접두 텍스트는 그대로다.
툴이 붙인 `_meta` 는 problem 과 함께 유지된다. 성공 result 와 JSON-RPC 프로토콜 오류
(`error.data`)는 바뀌지 않았다. 문서는 `docs/guide/08-mcp-integration.ko.md`(*툴 오류*).
- 커밋되는 개발 버전이 `0.1.0-SNAPSHOT` 이 됐다(종전에는 릴리스된 좌표 그대로인
`0.0.1-SNAPSHOT`). **GitHub Packages 의 개발 스냅샷도 이 좌표로 옮겨 간다** —
`org.htcom:protean:0.0.1-SNAPSHOT` 에 고정된 빌드는 더 이상 갱신을 받지 못하므로
Expand Down
33 changes: 30 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ While the version is `0.x`, the public API may change between minor releases.

## [Unreleased]

One addition to the wire contract of the console SSE stream (below); the Java API a
consumer compiles against is otherwise unchanged from `0.0.1` and still links. What
else changed is what the built-in MCP tools *answer*. If you wrap, subclass, or
Two wire-contract changes: an addition to the console SSE stream, and **a move of the
MCP tool-error problem detail from `structuredContent` to `_meta`** (both below). The
Java API a consumer compiles against gains one constant and is otherwise unchanged
from `0.0.1`, so it still links. What else changed is what the built-in MCP tools
*answer*. If you wrap, subclass, or
delegate to them, read the migration note at the end of this section.

### Added
Expand All @@ -31,9 +33,19 @@ delegate to them, read the migration note at the end of this section.
and clients must ignore fields they do not recognize. Existing clients are
unaffected — `EventSource` drops named events that have no listener. Documented in
`docs/guide/04-rest-api.md`.
- `McpToolResult.PROBLEM_META_KEY` (`"org.htcom.protean/problem"`) — the `_meta` key a
coded tool error now carries its problem detail under (see *Changed*).

### Fixed

- `protean.list_runtimes` declared `runtimes[].scope` as `string`, while its own
description says it is `null` when auto-provision is off — the default. Every call
on a default install therefore returned a result that violated the tool's
`outputSchema`: a client validating it reported `scope: null found, string
expected`, and with `protean.mcp.strict-schema=true` the dispatcher's own output
check turned the call into `OUTPUT_SCHEMA_VIOLATION`, so the tool could not be used
at all. `scope` is now declared `["string", "null"]`.

- `protean.list_modules`'s `trustTier` filter compared the argument (a `String`)
against `ModuleStatus.trustTier()` (a `TrustTier` enum), so `String.equals(Object)`
was false for every module and **the filter matched nothing for any value** — with
Expand Down Expand Up @@ -64,6 +76,21 @@ delegate to them, read the migration note at the end of this section.

### Changed

- **A coded MCP tool error now carries its RFC 9457 problem detail in the result
`_meta` under `org.htcom.protean/problem`, not in `structuredContent`.** On `0.0.1`
the problem object (`type`·`title`·`detail`·`code` and extension members) was
written to `structuredContent`, which the spec reserves for the payload the tool's
`outputSchema` describes and requires to conform to it. Every built-in tool that
declares an `outputSchema` was affected — for example `protean.scope_list` with
auto-provision off, or `protean.patch_module` on an unknown module. A client that
validates `structuredContent` on error results too — the TypeScript SDK's
`@modelcontextprotocol/sdk` 1.x does — replaced the actionable error with
`Structured content does not match the tool's output schema`, so the agent never
saw what went wrong. **Clients that read `structuredContent.code` on an error must
read `_meta["org.htcom.protean/problem"].code` instead**; the `[CODE]`-prefixed text
content is unchanged. A tool's own `_meta` is kept alongside the problem. Success
results and JSON-RPC protocol errors (`error.data`) are unchanged. Documented in
`docs/guide/08-mcp-integration.md` (*Tool errors*).
- The committed development version is now `0.1.0-SNAPSHOT` (it was `0.0.1-SNAPSHOT`,
left at the released coordinate). **Development snapshots on GitHub Packages move
with it** — a build pinned to `org.htcom:protean:0.0.1-SNAPSHOT` stops receiving
Expand Down
21 changes: 20 additions & 1 deletion docs/guide/08-mcp-integration.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,27 @@ tier 모델과 키별 의미는 [03. 설정](03-configuration.ko.md) 참고.

일반 모듈 대신 **LIBRARY** 모듈(shared-module 타입 공유)을 배포하려면 top-level 에 `kind: "LIBRARY"` 와 `exports`(발행할 패키지)를 주고 `controller` 는 생략한다; 라이브러리를 소비하는 모듈은 `uses`(라이브러리 모듈 id)를 추가한다. `controller` 처럼 `id`/`version` 과 같은 레벨에 둔다. [02. 모듈 작성 §8](02-module-authoring.ko.md) 참고.

### 툴 오류

게이트 거부·컴파일 실패 같은 도메인 실패는 JSON-RPC error 가 아니라 tool result 의 `isError: true`(+진단 텍스트)로 매핑된다. 프로토콜 수준 오류(알 수 없는 툴·필수 인자 누락 등)만 JSON-RPC error 로 나간다.

코드가 붙은 툴 오류는 RFC 9457 problem detail 을 `structuredContent` 가 **아니라** result **`_meta`** 의 `org.htcom.protean/problem` 키(상수 `McpToolResult.PROBLEM_META_KEY`)에 싣는다. 툴이 직접 붙인 `_meta` 키는 그대로 함께 나간다:

```json
{
"content": [{ "type": "text", "text": "[STATE_CONFLICT] scope tools require worker.db.auto-provision=true — …" }],
"isError": true,
"_meta": {
"org.htcom.protean/problem": {
"type": "urn:protean:error:state-conflict", "title": "State conflict",
"detail": "scope tools require worker.db.auto-provision=true — …", "code": "STATE_CONFLICT"
}
}
}
```

`structuredContent` 는 툴의 `outputSchema` 가 기술하는 페이로드 전용이다. 스펙은 그것이 스키마를 따르도록 요구하고, 일부 클라이언트는 오류 result 에서도 검증한다 — 거기에 problem 객체가 있으면 그 클라이언트는 실제 오류 대신 스키마 위반을 보고한다. 안정 `code`(와 `gate`·`moduleId`·`diagnostics` 같은 확장 멤버)는 `_meta["org.htcom.protean/problem"]` 에서 읽는다. `[CODE]` 접두 텍스트는 텍스트만 보여 주는 클라이언트를 위한 폴백이다.

## 인증·인가

**라이브러리는 인증을 구현하지 않는다.** 두 축으로 나뉜다.
Expand Down Expand Up @@ -424,7 +443,7 @@ claude mcp add --transport http protean http://localhost:8080/platform/mcp
- `structuredContent` 가 존재하고 **객체**인지
- 스키마의 **top-level `required`** 필드가 모두 있는지

둘 중 하나라도 어기면 조용히 흘리지 않고 `isError`("출력 스키마 위반: …") 로 막는다 — 잘못된 구조화 출력이 클라로 새는 버그 계열을 서버에서 차단하기 위함이다. `isError` result(도메인 실패)에는 이 검사를 적용하지 않는다(구조화 출력이 없는 게 정상).
둘 중 하나라도 어기면 조용히 흘리지 않고 `isError`("출력 스키마 위반: …") 로 막는다 — 잘못된 구조화 출력이 클라로 새는 버그 계열을 서버에서 차단하기 위함이다. `isError` result(도메인 실패)에는 이 검사를 적용하지 않는다(구조화 출력이 없는 게 정상) — 코드가 붙은 오류의 problem detail 은 `structuredContent` 가 아니라 `_meta` 에 실린다(위 [툴 오류](#툴-오류) 참고). 커스텀 툴이 오류 result 에 `structuredContent` 를 직접 붙인다면 그것도 스키마를 따라야 한다 — 일부 클라이언트는 거기서도 검증한다.

**기본값에선 중첩 필드·타입·형식 등 전체 JSON Schema 검증은 하지 않는다.** MCP 코어는 의도적으로 zero-dep 이라 스키마 밸리데이터를 끌어오지 않으며, 스펙도 전체 검증을 **클라이언트 몫**("Clients SHOULD validate structured results against this schema")으로 둔다. 즉 `outputSchema` 는 클라가 완전 검증할 수 있도록 **정확히 광고**하는 것이 서버의 역할이고, 기본 서버측 가드는 명백한 계약 위반(누락·비객체)만 잡는 안전망이다.

Expand Down
21 changes: 20 additions & 1 deletion docs/guide/08-mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,27 @@ Manage the DB **scope** lifecycle under `worker.db.auto-provision` — the MCP a

To deploy a **LIBRARY** module (shared-module typed sharing) instead of a normal one, add `kind: "LIBRARY"` and `exports` (the packages to publish) at the top level and omit `controller`; a module that consumes a library adds `uses` (the library module ids). These sit alongside `id`/`version`, the same as `controller`. See [02. Module Authoring §8](02-module-authoring.md).

### Tool errors

Domain failures such as a gate rejection or compile failure are not mapped to a JSON-RPC error but to a tool result with `isError: true` (+ diagnostic text). Only protocol-level errors (unknown tool, missing required argument, etc.) go out as JSON-RPC errors.

A coded tool error carries its RFC 9457 problem detail in the result **`_meta`** under the key `org.htcom.protean/problem` (the constant `McpToolResult.PROBLEM_META_KEY`), next to any `_meta` the tool attached — **not** in `structuredContent`:

```json
{
"content": [{ "type": "text", "text": "[STATE_CONFLICT] scope tools require worker.db.auto-provision=true — …" }],
"isError": true,
"_meta": {
"org.htcom.protean/problem": {
"type": "urn:protean:error:state-conflict", "title": "State conflict",
"detail": "scope tools require worker.db.auto-provision=true — …", "code": "STATE_CONFLICT"
}
}
}
```

`structuredContent` is reserved for the payload the tool's `outputSchema` describes. The spec requires it to conform to that schema, and some clients validate it on error results too — a problem object there would make them report a schema violation instead of the actual error. Read the stable `code` (and extension members such as `gate`, `moduleId`, `diagnostics`) from `_meta["org.htcom.protean/problem"]`; the `[CODE]`-prefixed text is the fallback for clients that surface only text.

## Authentication & authorization

**The library implements no authentication.** It splits into two axes.
Expand Down Expand Up @@ -429,7 +448,7 @@ When a tool that declared `outputSchema` returns **success**, the dispatcher che
- that `structuredContent` exists and is an **object**
- that all of the schema's **top-level `required`** fields are present

If either is violated, it does not silently pass but blocks with `isError` ("output schema violation: …") — to stop, at the server, the class of bugs where malformed structured output leaks to the client. This check is not applied to `isError` results (domain failures), where the absence of structured output is normal.
If either is violated, it does not silently pass but blocks with `isError` ("output schema violation: …") — to stop, at the server, the class of bugs where malformed structured output leaks to the client. This check is not applied to `isError` results (domain failures), where the absence of structured output is normal — a coded error's problem detail goes in `_meta`, not `structuredContent` (see [Tool errors](#tool-errors) above). A custom tool that does attach `structuredContent` to an error result must keep it conformant too, since some clients validate it there as well.

**By default it does not perform full JSON Schema validation of nested fields, types, formats, etc.** The MCP core is deliberately zero-dep and does not pull in a schema validator, and the spec too leaves full validation to the **client** ("Clients SHOULD validate structured results against this schema"). That is, the server's job is to **advertise `outputSchema` accurately** so the client can validate fully, and the default server-side guard is a safety net that catches only blatant contract violations (missing/non-object).

Expand Down
24 changes: 14 additions & 10 deletions src/main/java/org/htcom/protean/mcp/McpDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -626,16 +626,20 @@ private JsonNode toolResultNode(McpToolResult r) {
if (r.isError()) {
res.put("isError", true);
}
// Success: the tool payload. Error (a problem is attached): the RFC 9457 problem-detail shape as structuredContent.
JsonNode structured = r.structured();
if (structured == null && r.problem() != null) {
structured = r.problem().toJson(mapper);
}
if (structured != null) {
res.set("structuredContent", structured);
}
if (r.meta() != null) {
res.set("_meta", r.meta()); // additional metadata attached by the tool
if (r.structured() != null) {
res.set("structuredContent", r.structured());
}
// A stable-code error's RFC 9457 problem goes in _meta, never in structuredContent: a tool's outputSchema
// describes its success payload, the spec requires structuredContent to conform to it, and clients that
// validate it on error results too would replace the actionable error with a schema-violation error.
JsonNode meta = r.meta(); // additional metadata attached by the tool
if (r.problem() != null) {
ObjectNode merged = meta instanceof ObjectNode o ? o.deepCopy() : mapper.createObjectNode();
merged.set(McpToolResult.PROBLEM_META_KEY, r.problem().toJson(mapper));
meta = merged;
}
if (meta != null) {
res.set("_meta", meta);
}
return res;
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/org/htcom/protean/mcp/McpTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ default String title() {
* Output JSON Schema (optional). When declared, a successful result's {@code structuredContent} must conform
* to it; the dispatcher validates a minimal fit (is-object and top-level {@code required}) and blocks with
* {@code isError} on a mismatch (guarding against structuredContent-class bugs). Full schema validation is the
* client's responsibility (per spec). {@code null} = not declared.
* client's responsibility (per spec). Some clients validate {@code structuredContent} on error results too, so a
* tool that attaches {@code structuredContent} to an {@code isError} result must keep it conformant as well;
* stable-code errors never do — their problem detail travels in {@code _meta} under
* {@link McpToolResult#PROBLEM_META_KEY}. {@code null} = not declared.
*/
default ObjectNode outputSchema() {
return null;
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/org/htcom/protean/mcp/McpToolResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
* (ModuleStatus) or a {@code jobId} union. Currently it always returns only the terminal state.
*
* <p>Stable-code errors ({@link #error(ErrorCode, String)}) carry an RFC 9457 shape in {@code problem}, and the
* boundary ({@code McpDispatcher.toolResultNode}) serializes it into {@code structuredContent}. The mapping lives
* in one boundary — the mapper is not placed here. Plain-text errors without a {@code problem} are promoted to
* codes in a later phase.
* boundary ({@code McpDispatcher.toolResultNode}) serializes it into the result {@code _meta} under
* {@link #PROBLEM_META_KEY} — not into {@code structuredContent}, which is reserved for the payload described by
* the tool's {@code outputSchema}. The mapping lives in one boundary — the mapper is not placed here. Plain-text
* errors without a {@code problem} are promoted to codes in a later phase.
*/
public record McpToolResult(String text, boolean isError, JsonNode structured, JsonNode meta, ProblemDetail problem) {

/** {@code _meta} key under which a stable-code error result carries its RFC 9457 problem detail. */
public static final String PROBLEM_META_KEY = "org.htcom.protean/problem";

public static McpToolResult ok(String text) {
return new McpToolResult(text, false, null, null, null);
}
Expand All @@ -41,7 +45,7 @@ public static McpToolResult error(String text) {

/**
* Stable-code error (RFC 9457). {@code text} is a {@code [CODE]}-prefixed detail for lossy harnesses, and
* {@code problem} is the problem-detail shape that the boundary serializes into {@code structuredContent}.
* {@code problem} is the problem-detail shape that the boundary serializes into {@code _meta[PROBLEM_META_KEY]}.
* Attach structured remediation data with {@link #with} (e.g. {@code gate}, {@code diagnostics}).
*/
public static McpToolResult error(ErrorCode code, String detail) {
Expand Down
Loading
Loading