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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,34 @@ jobs:

- name: Package
run: mvn -B package -DskipTests

- name: Test core
run: mvn -B test

mcp-ci:
name: MCP adapter CI
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
cache: maven

- name: Install matching core artifact
run: |
adapter_version="$(mvn -q -f mcp/pom.xml help:evaluate -Dexpression=project.version -DforceStdout)"
mvn -B versions:set -DnewVersion="${adapter_version}" -DgenerateBackupPoms=false
mvn -B install -DskipTests

- name: Test and install optional MCP adapter
run: mvn -B -f mcp/pom.xml install

- name: Compile self-hosted MCP example
run: mvn -B -f examples/self_hosted_mcp_worker/pom.xml package
44 changes: 35 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Java Release

# Publishes com.volcengine:ark-runtime to Maven Central. Tags are created
# Publishes com.volcengine:ark-runtime and the optional ark-runtime-mcp adapter
# to Maven Central. Tags are created
# exclusively by ark-hand after a sync PR lands on main, so a pushed v* tag
# is always a reviewed release snapshot whose tree matches the internal
# source tree. Mirrors the proven setup from volcengine/volcengine-java-sdk:
Expand Down Expand Up @@ -58,7 +59,7 @@ jobs:
with:
ref: ${{ steps.tag.outputs.name }}

- name: Verify pom version matches tag
- name: Verify artifact versions match tag
env:
RELEASE_TAG: ${{ steps.tag.outputs.name }}
run: |
Expand All @@ -72,27 +73,41 @@ jobs:
echo "::error::pom.xml ark-runtime version ${actual} does not match tag ${RELEASE_TAG}"
exit 1
fi
mcp_actual=$(python3 -c "
import re
text = open('mcp/pom.xml').read()
artifact = re.search(r'<artifactId>ark-runtime-mcp</artifactId>\s*<version>([^<]+)</version>', text)
dependency = re.search(r'<ark-runtime.version>([^<]+)</ark-runtime.version>', text)
print((artifact.group(1) if artifact else 'missing') + ' ' + (dependency.group(1) if dependency else 'missing'))
")
if [ "${mcp_actual}" != "${version} ${version}" ]; then
echo "::error::mcp artifact/core versions ${mcp_actual} do not match tag ${RELEASE_TAG}"
exit 1
fi

- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: "8"
java-version: "17"
distribution: "temurin"
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_TOKEN
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: GPG_PASSPHRASE

- name: Check Maven Central version
- name: Check Maven Central versions
id: maven_check
env:
RELEASE_TAG: ${{ steps.tag.outputs.name }}
run: |
version="${RELEASE_TAG#v}"
pom_url="https://repo.maven.apache.org/maven2/com/volcengine/ark-runtime/${version}/ark-runtime-${version}.pom"
code=$(curl -sS -o /dev/null -w '%{http_code}' "${pom_url}")
echo "publish_needed=$([ "${code}" = "200" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
core_url="https://repo.maven.apache.org/maven2/com/volcengine/ark-runtime/${version}/ark-runtime-${version}.pom"
mcp_url="https://repo.maven.apache.org/maven2/com/volcengine/ark-runtime-mcp/${version}/ark-runtime-mcp-${version}.pom"
core_code=$(curl -sS -o /dev/null -w '%{http_code}' "${core_url}")
mcp_code=$(curl -sS -o /dev/null -w '%{http_code}' "${mcp_url}")
echo "core_publish_needed=$([ "${core_code}" = "200" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
echo "mcp_publish_needed=$([ "${mcp_code}" = "200" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"

- name: Check publish credentials
env:
Expand All @@ -111,8 +126,8 @@ jobs:
exit 1
fi

- name: Publish to Maven Central
if: steps.maven_check.outputs.publish_needed == 'true'
- name: Publish core to Maven Central
if: steps.maven_check.outputs.core_publish_needed == 'true'
run: |
mvn clean deploy -B -Ppublic -DskipTests \
-Dmaven.javadoc.failOnError=false \
Expand All @@ -121,3 +136,14 @@ jobs:
MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_CENTRAL_TOKEN: ${{ secrets.OSSRH_TOKEN }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

- name: Publish MCP adapter to Maven Central
if: steps.maven_check.outputs.mcp_publish_needed == 'true'
run: |
mvn clean deploy -B -f mcp/pom.xml -Ppublic -DskipTests \
-Dmaven.javadoc.failOnError=false \
-Dmaven.javadoc.quiet=true
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_CENTRAL_TOKEN: ${{ secrets.OSSRH_TOKEN }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
13 changes: 7 additions & 6 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ from third-party open-source projects.

## Anthropic self-hosted worker SDK

Portions of the self-hosted worker lifecycle and local agent tool
implementations under
`src/main/java/com/volcengine/ark/runtime/selfhosted`, including the work
poller, environment worker, session tool runner, skill initializer, local
tools, and tool-result store, are structurally adapted from Anthropic's
self-hosted worker SDK implementations:
Portions of the self-hosted worker lifecycle, local agent tool, and client-side
MCP helper implementations under
`src/main/java/com/volcengine/ark/runtime/selfhosted` and `mcp`, including the
work poller, environment worker, session tool runner, skill initializer, local
tools, tool-result store, and MCP conversion helpers, are structurally adapted
from Anthropic's SDK implementations:

- https://github.com/anthropics/anthropic-sdk-go
- https://github.com/anthropics/anthropic-sdk-python
- https://github.com/anthropics/anthropic-sdk-java

The upstream projects are licensed under the MIT License. The MIT copyright
and permission notice is preserved below as required by that license.
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ All service-calling examples are grouped by cloud:
The paired multimodal and sparse embedding examples default to `doubao-embedding-vision-251215` / `skylark-embedding-vision-251215`. The paired image examples default to `doubao-seedream-5-0-pro-260628` / `dola-seedream-5-0-pro-260628`. The paired video-generation examples default to `doubao-seedance-2-0-fast-260128` / `dreamina-seedance-2-0-fast-260128`.

MCP is available in both clouds and its calls explicitly send `ark-beta-mcp: true`. Other built-in tools are CN-only: Knowledge Search sends `ark-beta-knowledge-search: true`, and Doubao App sends `ark-beta-doubao-app: true`.

[`self_hosted_mcp_worker/`](./self_hosted_mcp_worker) demonstrates how to discover tools from a local stdio MCP server, convert them into Agent custom tool declarations, and execute them through the self-hosted worker.
124 changes: 124 additions & 0 deletions examples/self_hosted_mcp_worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Self-hosted MCP worker

This example follows Anthropic's client-side MCP helper example at the same
level of abstraction: connect to an MCP server, discover its tools, convert
them, and run an existing self-hosted Environment Worker.

The self-hosted Environment must exist before starting the worker. Create or
update an Agent with the printed `Agent custom tool` declarations before
creating a Session. Printing declarations does not update the Agent
automatically. The same MCP tool list is registered with the worker for
execution, and the example reads every `tools/list` page.

Build the standalone example after `ark-runtime` and `ark-runtime-mcp` 0.6.0
are available in Maven Central or installed in the local Maven repository:

```bash
mvn -B -f examples/self_hosted_mcp_worker/pom.xml package
```

This standalone POM resolves those artifacts from Maven; it is not a reactor
build of the sibling core and `mcp` source directories.

## Manual end-to-end verification

The example registers the MCP tool implementation with the self-hosted worker,
but it does not create or update Managed Agents resources. Complete the
following control-plane fields manually:

1. Create a self-hosted Environment and copy its ID into
`MA_ENVIRONMENT_ID`.
2. Set `ARK_API_KEY`. Set `ARK_BASE_URL` only when using a non-production
endpoint.
3. Start the worker with the MCP server command after `--`:

```bash
export ARK_API_KEY=...
export MA_ENVIRONMENT_ID=env_xxx
# Optional, for example when testing against staging:
# export ARK_BASE_URL=https://example.com/api/v3

JAR=examples/self_hosted_mcp_worker/target/ark-runtime-self-hosted-mcp-example-0.6.0-jar-with-dependencies.jar
java -jar "$JAR" -- java -cp "$JAR" \
com.volcengine.ark.runtime.examples.selfhostedmcp.McpEchoServer
```

4. Copy every printed `Agent custom tool: {...}` declaration into the Agent's
tool configuration. For the bundled server, use the declaration below.
Configure it before creating the Session; printing the declaration does not
update the Agent automatically.
5. Create a Session that uses both that Agent and the same self-hosted
Environment from `MA_ENVIRONMENT_ID`.
6. Send a message such as:

```text
Call mcp_echo exactly once with text "Hello from MCP echo!" and report the result.
```

The verification passes when the Session shows an `mcp_echo` call with that
input, a `user.custom_tool_result` containing
`MCP echo: Hello from MCP echo!`, a final Agent response, and a final
`session.status_idle` whose stop reason is `end_turn`. A temporary
`session.status_idle` with stop reason `requires_action` means that the Session
is waiting for the external custom-tool result; it is expected and is not an
approval prompt or a failure. At the event level, observe these milestones:

```text
agent.custom_tool_use
session.status_idle stop_reason=requires_action
user.custom_tool_result posted by the worker
agent.message
session.status_idle stop_reason=end_turn
```

Do not depend on the first idle event and the tool-result POST being displayed
in an exact relative order: the worker starts executing as soon as it observes
`agent.custom_tool_use`.

Keep the worker process running for the whole verification. The command after
`--` is a stdio MCP server command, not a URL; the worker starts the process and
communicates with it through stdin/stdout.

The bundled server exposes this declaration:

```json
{
"type": "custom",
"name": "mcp_echo",
"description": "Echo text through the local MCP server.",
"input_schema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"]
}
}
```

To use another stdio MCP server, replace the command after `--`. Set
`ARK_BASE_URL` only when overriding the SDK's production endpoint. The example
removes `ARK_API_KEY` from the MCP subprocess environment, but inherits other
environment variables. Review or allowlist them before production and use
separate MCP-specific credentials.

The example opens one MCP process and client session for the lifetime of the
Environment Worker and reuses it for every Managed Agents Session handled by
that worker. MCP calls do not automatically contain the Managed Agents
`session_id` or `work_id`, and Session idle/deletion is not an MCP lifecycle
notification. Use a stateless MCP server or implement explicit tenant/session
isolation, and expect the MCP process to stop only when the worker exits. The
command-line example accepts a stdio child command only; other transports can
be used by constructing an MCP client session programmatically.

Managed Agents currently accepts at most eight custom tools per Agent. If the
server exposes more, select the same stable subset for both the Agent and the
worker. Custom tools do not use Managed Agents permission policies: the worker
executes matching calls directly, so put approval, authorization, and operation
allowlists in the MCP server or wrapper. Only connect trusted servers, avoid
tool names that collide with built-in Agent tools, and configure an MCP client
timeout. MCP servers run with the worker's OS, filesystem, and network
permissions rather than in a Managed Agents sandbox, so run them with least
privilege and do not pass `ARK_API_KEY` to them. Update the Agent while it is
idle and restart the worker whenever the server's tool list changes.

This directory is a separate Java 17 module so the optional MCP dependency does
not change the core SDK's Java 8 baseline.
60 changes: 60 additions & 0 deletions examples/self_hosted_mcp_worker/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.volcengine</groupId>
<artifactId>ark-runtime-self-hosted-mcp-example</artifactId>
<version>0.6.0</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<ark-runtime.version>0.6.0</ark-runtime.version>
<maven-assembly-plugin.version>3.7.1</maven-assembly-plugin.version>
<maven-compiler-plugin.version>3.14.1</maven-compiler-plugin.version>
</properties>

<dependencies>
<dependency>
<groupId>com.volcengine</groupId>
<artifactId>ark-runtime-mcp</artifactId>
<version>${ark-runtime.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly-plugin.version}</version>
<configuration>
<archive>
<manifest>
<mainClass>com.volcengine.ark.runtime.examples.selfhostedmcp.SelfHostedMcpWorkerExample</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-example</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
// SPDX-License-Identifier: Apache-2.0

package com.volcengine.ark.runtime.examples.selfhostedmcp;

import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

/** Minimal stdio MCP server used by {@link SelfHostedMcpWorkerExample}. */
public final class McpEchoServer {
private McpEchoServer() {
}

/** Starts a single echo tool over stdio. */
public static void main(String[] args) throws InterruptedException {
Map<String, Object> textProperty = new LinkedHashMap<>();
textProperty.put("type", "string");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("text", textProperty);
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
schema.put("properties", properties);
schema.put("required", Collections.singletonList("text"));

StdioServerTransportProvider transport =
new StdioServerTransportProvider(McpJsonDefaults.getMapper());
McpSyncServer server = McpServer.sync(transport)
.serverInfo("ark-self-hosted-mcp-example", "1.0.0")
.toolCall(
McpSchema.Tool.builder("mcp_echo", schema)
.description("Echo text through the local MCP server.")
.build(),
(exchange, request) -> McpSchema.CallToolResult.builder()
.addTextContent("MCP echo: " + request.arguments().get("text"))
.build())
.build();
Runtime.getRuntime().addShutdownHook(new Thread(server::close, "mcp-echo-server-shutdown"));
new CountDownLatch(1).await();
}
}
Loading
Loading