Skip to content
Open
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
29 changes: 9 additions & 20 deletions claude-code-sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.markpollack</groupId>
<artifactId>claude-agent-sdk-parent</artifactId>
<version>1.8.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand All @@ -31,15 +31,7 @@
<artifactId>zt-exec</artifactId>
</dependency>

<!-- JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JSON annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
Expand All @@ -63,16 +55,13 @@
<artifactId>mcp</artifactId>
</dependency>

<!-- Jackson 3 (tools.jackson) — this SDK does not import tools.jackson in its
own source, but mcp 2.0.0 requires mcp-json-jackson3 at runtime, so these
artifacts are part of the closure every consumer of claude-code-sdk
receives. The parent's tools.jackson BOM import governs the reactor only:
flattenMode=ossrh strips <dependencyManagement> from the published POM, so
without these direct declarations a no-BOM consumer resolves the vulnerable
3.0.3 that mcp drags in transitively. Declared here so the floor travels.
jackson-dataformat-yaml is included because Jackson 3 requires every module
to sit on the same minor; leaving it at 3.0.3 beside a 3.1.x core is the
version skew the floor itself would otherwise introduce. -->
<!-- JSON processing — Jackson 3 (tools.jackson), the SDK's JSON stack and also
required by mcp at runtime. The parent's tools.jackson BOM governs the
reactor only: flattenMode=ossrh strips <dependencyManagement> from the
published POM, so these direct declarations are what carry the version
floor to a no-BOM consumer. jackson-dataformat-yaml is included because
Jackson 3 requires every module to sit on the same minor, and it reaches
consumers through mcp's json-schema-validator. -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package io.github.markpollack.claude.agent.sdk;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.TransportException;
Expand All @@ -39,6 +38,7 @@
import io.github.markpollack.claude.agent.sdk.permission.PermissionResult;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionCallback;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionContext;
import tools.jackson.databind.ObjectMapper;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package io.github.markpollack.claude.agent.sdk;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.TransportException;
Expand All @@ -43,6 +42,7 @@
import io.github.markpollack.claude.agent.sdk.permission.PermissionResult;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionCallback;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionContext;
import tools.jackson.databind.ObjectMapper;

import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

package io.github.markpollack.claude.agent.sdk.mcp;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;

import java.util.LinkedHashMap;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
* In-process SDK servers are managed by the Java SDK and communicate via the mcp_message
* control protocol.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = McpServerConfig.McpStdioServerConfig.class,
visible = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
defaultImpl = McpServerConfig.McpStdioServerConfig.class, visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = McpServerConfig.McpStdioServerConfig.class, name = "stdio"),
@JsonSubTypes.Type(value = McpServerConfig.McpSseServerConfig.class, name = "sse"),
@JsonSubTypes.Type(value = McpServerConfig.McpHttpServerConfig.class, name = "http"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@

package io.github.markpollack.claude.agent.sdk.parsing;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
import io.github.markpollack.claude.agent.sdk.types.Message;
import io.github.markpollack.claude.agent.sdk.types.RateLimitEvent;
import io.github.markpollack.claude.agent.sdk.types.control.ControlRequest;
import io.github.markpollack.claude.agent.sdk.types.control.ControlResponse;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

/**
* Parser for Claude CLI bidirectional control protocol messages. This parser handles both
Expand Down Expand Up @@ -80,7 +80,7 @@ public ControlMessageParser() {
* @param maxBufferSize maximum message size in bytes (for buffer overflow protection)
*/
public ControlMessageParser(int maxBufferSize) {
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.objectMapper = new ObjectMapper();
this.messageParser = new MessageParser();
this.maxBufferSize = maxBufferSize > 0 ? maxBufferSize : DEFAULT_MAX_BUFFER_SIZE;
}
Expand All @@ -99,7 +99,7 @@ public ControlMessageParser(ObjectMapper objectMapper) {
* @param maxBufferSize maximum message size in bytes (for buffer overflow protection)
*/
public ControlMessageParser(ObjectMapper objectMapper, int maxBufferSize) {
this.objectMapper = objectMapper.copy().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.objectMapper = objectMapper.rebuild().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
this.messageParser = new MessageParser();
this.maxBufferSize = maxBufferSize > 0 ? maxBufferSize : DEFAULT_MAX_BUFFER_SIZE;
}
Expand Down Expand Up @@ -127,7 +127,7 @@ public ParsedMessage parse(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseFromNode(root, json);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
Expand All @@ -149,11 +149,11 @@ public int getMaxBufferSize() {
*/
public ParsedMessage parseFromNode(JsonNode node, String originalJson) throws MessageParseException {
JsonNode typeNode = node.get("type");
if (typeNode == null || !typeNode.isTextual()) {
if (typeNode == null || !typeNode.isString()) {
throw new MessageParseException("Missing or invalid 'type' field in message");
}

String type = typeNode.asText();
String type = typeNode.asString();

if (TYPE_CONTROL_REQUEST.equals(type)) {
return parseControlRequest(node, originalJson);
Expand Down Expand Up @@ -185,7 +185,7 @@ private ParsedMessage parseControlRequest(JsonNode node, String originalJson) th

return ParsedMessage.Control.of(request);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw new MessageParseException("Failed to parse control request: " + e.getMessage(), e);
}
}
Expand All @@ -205,7 +205,7 @@ private ParsedMessage parseControlResponse(JsonNode node, String originalJson) t

return ParsedMessage.ControlResponseMessage.of(response);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw new MessageParseException("Failed to parse control response: " + e.getMessage(), e);
}
}
Expand All @@ -222,7 +222,7 @@ private ParsedMessage parseRateLimitEvent(JsonNode node) throws MessageParseExce
event.rateLimitInfo() != null ? event.rateLimitInfo().resetsAt() : 0);
return ParsedMessage.RateLimitEventMessage.of(event);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw new MessageParseException("Failed to parse rate_limit_event: " + e.getMessage(), e);
}
}
Expand Down Expand Up @@ -255,9 +255,9 @@ public boolean isControlRequest(String json) {
try {
JsonNode root = objectMapper.readTree(json);
JsonNode typeNode = root.get("type");
return typeNode != null && TYPE_CONTROL_REQUEST.equals(typeNode.asText());
return typeNode != null && TYPE_CONTROL_REQUEST.equals(typeNode.asString());
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
return false;
}
}
Expand All @@ -276,9 +276,9 @@ public String extractRequestId(String json) {
try {
JsonNode root = objectMapper.readTree(json);
JsonNode requestIdNode = root.get("request_id");
return requestIdNode != null && requestIdNode.isTextual() ? requestIdNode.asText() : null;
return requestIdNode != null && requestIdNode.isString() ? requestIdNode.asString() : null;
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
import io.github.markpollack.claude.agent.sdk.types.ResultMessage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -84,7 +84,7 @@ public ResultMessage parseJsonResult(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseResultFromNode(root);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
Expand Down Expand Up @@ -152,7 +152,7 @@ private Map<String, Object> parseUsageMap(JsonNode usageNode) {
}

// Parse all usage fields
usageNode.fields().forEachRemaining(entry -> {
usageNode.properties().forEach(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();

Expand All @@ -162,19 +162,19 @@ private Map<String, Object> parseUsageMap(JsonNode usageNode) {
else if (value.isDouble()) {
usage.put(key, value.asDouble());
}
else if (value.isTextual()) {
usage.put(key, value.asText());
else if (value.isString()) {
usage.put(key, value.asString());
}
else if (value.isObject()) {
// Handle nested objects like server_tool_use
Map<String, Object> nestedMap = new HashMap<>();
value.fields().forEachRemaining(nestedEntry -> {
value.properties().forEach(nestedEntry -> {
JsonNode nestedValue = nestedEntry.getValue();
if (nestedValue.isInt()) {
nestedMap.put(nestedEntry.getKey(), nestedValue.asInt());
}
else if (nestedValue.isTextual()) {
nestedMap.put(nestedEntry.getKey(), nestedValue.asText());
else if (nestedValue.isString()) {
nestedMap.put(nestedEntry.getKey(), nestedValue.asString());
}
else {
nestedMap.put(nestedEntry.getKey(), nestedValue.toString());
Expand All @@ -192,7 +192,7 @@ else if (nestedValue.isTextual()) {

private String getStringField(JsonNode node, String fieldName) {
JsonNode field = node.get(fieldName);
return (field != null && !field.isNull()) ? field.asText() : null;
return (field != null && !field.isNull()) ? field.asString() : null;
}

private int getIntField(JsonNode node, String fieldName, int defaultValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
import io.github.markpollack.claude.agent.sdk.types.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -51,7 +51,7 @@ public Message parseMessage(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseMessageFromNode(root);
}
catch (JsonProcessingException e) {
catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
Expand All @@ -60,6 +60,15 @@ public Message parseMessage(String json) throws MessageParseException {
* Parses a JsonNode into a Message object.
*/
public Message parseMessageFromNode(JsonNode node) throws MessageParseException {
try {
return tryParseMessageFromNode(node);
}
catch (JacksonException e) {
throw new MessageParseException("Unexpected error when reading json fields: " + e.getMessage(), e);
}
}

private Message tryParseMessageFromNode(JsonNode node) throws MessageParseException {
String type = getStringField(node, "type");
if (type == null) {
throw new MessageParseException("Missing 'type' field in message");
Expand Down Expand Up @@ -91,8 +100,8 @@ private UserMessage parseUserMessage(JsonNode node) throws MessageParseException
throw new MessageParseException("Missing 'content' field in user message");
}

if (contentNode.isTextual()) {
return UserMessage.of(contentNode.asText());
if (contentNode.isString()) {
return UserMessage.of(contentNode.asString());
}
else if (contentNode.isArray()) {
List<ContentBlock> blocks = parseContentBlocks(contentNode);
Expand Down Expand Up @@ -222,8 +231,8 @@ private ToolResultBlock parseToolResultBlock(JsonNode node) throws MessageParseE
JsonNode contentNode = node.get("content");
Object content = null;
if (contentNode != null) {
if (contentNode.isTextual()) {
content = contentNode.asText();
if (contentNode.isString()) {
content = contentNode.asString();
}
else if (contentNode.isArray()) {
content = parseDataList(contentNode);
Expand All @@ -246,7 +255,7 @@ else if (content instanceof List) {

private Map<String, Object> parseDataMap(JsonNode node) {
Map<String, Object> map = new HashMap<>();
node.fields().forEachRemaining(entry -> {
node.properties().forEach(entry -> {
map.put(entry.getKey(), parseJsonValue(entry.getValue()));
});
return map;
Expand All @@ -261,8 +270,8 @@ private List<Object> parseDataList(JsonNode node) {
}

private Object parseJsonValue(JsonNode node) {
if (node.isTextual()) {
return node.asText();
if (node.isString()) {
return node.asString();
}
else if (node.isNumber()) {
return node.isInt() ? node.asInt() : node.asDouble();
Expand All @@ -288,7 +297,7 @@ private Map<String, Object> parseUsageMap(JsonNode node) {
// Utility methods for safe field extraction
private String getStringField(JsonNode node, String fieldName) {
JsonNode field = node.get(fieldName);
return field != null && field.isTextual() ? field.asText() : null;
return field != null && field.isString() ? field.asString() : null;
}

private int getIntField(JsonNode node, String fieldName, int defaultValue) {
Expand All @@ -311,4 +320,4 @@ private Double getDoubleField(JsonNode node, String fieldName) {
return field != null && field.isNumber() ? field.asDouble() : null;
}

}
}
Loading