KDCube Claude Code Integration
A bundle-scoped Claude Code subprocess runner with SDK-managed workspace files, MCP server config, KDCube skill materialization, communicator streaming, and standard KDCube LLM accounting.
What This Integration Is
Claude Code support in KDCube is a subagent runtime for bundles. React remains the primary chat/task decision loop. A bundle can delegate scoped work to Claude Code when the job benefits from Claude's own file-oriented loop, built-in tools, and MCP client support.
The SDK owns the reusable runner and workspace file generation. The bundle still owns product policy: which MCP servers to expose, which tools are allowed, which instructions are safe, which secrets are passed, and which workspace is appropriate for the current user/task boundary.
Important distinction: Claude Code does not automatically inherit React tools or React skills. If both runtimes need the same capability, expose it separately for each runtime.
| Runtime | Tool source | Skill source | Typical use |
|---|---|---|---|
| React | tools_descriptor.py, MCP_TOOL_SPECS, SDK tools |
skills_descriptor.py and bundle skills/ |
Main chat turns, task executions, transport-backed assistant work |
| Claude Code | Claude built-ins plus generated Claude MCP config | CLAUDE.md and native Claude project Skills under .claude/skills |
Scoped code/file/research subagent work, specialized MCP loops, private sub-processing |
Runtime Model
The runner starts a fresh claude -p subprocess for each turn and uses a
deterministic Claude session id for continuity. That session id is derived from the
current KDCube user, conversation, and Claude agent name, so two users do not collide
while the same user can keep separate Claude sessions by using separate conversations
or agent names.
Fresh subprocess per turn
The SDK runs Claude Code in print mode with stream-json. Follow-up and steer turns resume the same Claude session identity.
Explicit config
The caller provides workspace_path, model, allowed tools, permission mode, timeout, env, and optional workspace config.
Communicator streaming
The runner emits chat.step and chat.delta events through the normal KDCube communicator path.
Core API surface
| Input | SDK surface | Meaning |
|---|---|---|
| Agent identity | agent_name | Part of session identity and accounting metadata |
| Workspace | workspace_path | Claude subprocess working directory |
| Additional paths | additional_directories | Passed to Claude as --add-dir |
| Built-in tools | allowed_tools | Forwarded as Claude --allowedTools |
| Workspace files | ClaudeCodeWorkspaceConfig | Lets the SDK write .mcp.json, settings, CLAUDE.md, and project Skills |
| Structured progress | structured_output_prefixes | Parses caller-defined line-framed JSON records from streamed text |
| Timeout | timeout_seconds | Marks the run failed and terminates when exceeded |
Workspace And Session Continuity
The bundle chooses the local workspace root. The SDK can optionally hydrate and publish Claude's own session files through a local or git-backed session store, but KDCube conversation JSON is not Claude's continuity substrate.
-
1
Choose a deterministic workspace
Use a per-user, per-task, or per-conversation path that matches the privacy boundary of the work.
-
2
Prepare support files
The SDK writes standard Claude workspace files from
ClaudeCodeWorkspaceConfig. -
3
Run Claude
The subprocess runs from
workspace_pathand receives configured--add-dirpaths. -
4
Persist continuity if enabled
The git-backed store uses one branch per tenant, project, user, conversation, and agent.
Workspace scoping is not security isolation. workspace_path means "run Claude from this directory." additional_directories means "also pass these paths through --add-dir." It is not a chroot, container, filesystem jail, or privilege boundary.
MCP Integration
There are three different MCP surfaces. Keeping them separate prevents most integration mistakes.
| Concept | Configured by | Consumer |
|---|---|---|
| React MCP client config | Bundle props config.mcp.services plus MCP_TOOL_SPECS | KDCube tool subsystem |
| Bundle-served MCP endpoint | @mcp(...) on the bundle entrypoint | External MCP clients, Claude Code, other services |
| Claude Code MCP config | Generated .mcp.json in the Claude workspace | The claude CLI subprocess |
An @mcp(...) endpoint exposes a bundle MCP server. It does not automatically
tell Claude how to connect to it. The bundle should pass a ClaudeCodeWorkspaceConfig
with mcp_servers, enabled servers, and allowed tool names so the SDK can write
Claude-compatible workspace files.
Reachability rule: the MCP URL must be reachable from the process or container that runs claude. 127.0.0.1 is correct only when the MCP server and Claude client share the same network namespace.
Deployment reachability examples
- Same local process/container:
http://127.0.0.1:<port>can work. - Different Docker containers: use Docker service DNS or another internal host name.
- ECS same task with shared task networking: localhost can work if the target container listens there.
- ECS separate task/service: use Cloud Map, an internal ALB, or another private endpoint.
Skills: React vs Claude Code
React skills and Claude Code Skills are not the same object at runtime. KDCube now bridges
them by letting bundles pass KDCube skill ids to ClaudeCodeWorkspaceConfig.skill_ids.
The SDK resolves those ids through the active skills subsystem, expands imports, copies
support files, and writes native Claude Code project Skills under .claude/skills.
CLAUDE.md
Broad workspace instruction. Use it for global constraints, task policy, and scenario rules.
Project Skill
Discoverable capability folder. Claude loads the full SKILL.md only when the task matches the skill description.
workspace/
.mcp.json
CLAUDE.md
.claude/
settings.local.json
skills/
email-analysis/
SKILL.md
reference.md
scripts/
helper.py
Tool access is still separate. A Claude Skill can guide behavior, but Claude can only call tools that the workspace MCP config and Claude tool allow-list make available.
Streaming And Accounting
Claude Code runs emit normal KDCube LLM accounting events. The accounting metadata marks
the runtime as claude_code, while usage comes from the Claude CLI stream.
Usage
Input, output, thinking, cache read, cache creation, request count, and provider-reported cost when available.
Result fields
Bundles can persist model, usage, cost_usd, structured_events, and final_text.
Progress
chat.step shows start/stderr/completion, while chat.delta streams incremental text.
service_type = "llm"
provider = "anthropic"
metadata.runtime = "claude_code"
metadata.agent_name = "email-processor"
metadata.claude_session_id = "<stable uuid>"
Safety Boundary And Runtime Requirements
Claude Code is not sandboxed by this SDK. It is a subprocess in the same OS/container security boundary as the runner. If you allow Bash, broad Read, broad Grep, or sensitive env variables, assume Claude can use them.
Minimum deployment requirements
- The
claudeCLI exists in the runtime image/container. - Anthropic/Claude credentials are available to the subprocess only when intentionally provided.
- The Claude workspace is writable.
- The bundle MCP endpoint is enabled when Claude needs bundle tools.
- Claude can reach the MCP URL over HTTP from its own network namespace.
- The MCP endpoint authenticates itself; proc does not authenticate bundle MCP on its behalf.
- FastMCP endpoints are stateless or their lifespan/session manager is correctly initialized.
Policy owned by the bundle
- Resolve and pass only deliberate short-lived env values or tokens.
- Allow only the Claude built-ins and MCP tools required for the run.
- Use run-scoped MCP tokens with user id, run id, scope, and expiry.
- Do not write durable secrets into
.mcp.json, logs, tool outputs, or bundle props. - Use a deterministic workspace path that matches the user/task privacy boundary.
Bundle Integration Pattern
This is the current shape: build a workspace config, create a Claude Code agent from the active KDCube request context, and run it through the SDK turn wrapper.
from kdcube_ai_app.apps.chat.sdk.solutions.claude_code import (
ClaudeCodeAgent,
ClaudeCodeSessionStoreConfig,
ClaudeCodeWorkspaceConfig,
run_claude_code_turn,
)
workspace_config = ClaudeCodeWorkspaceConfig(
mcp_servers={
"scoped_data": {
"type": "http",
"url": mcp_url,
"headers": {"X-Run-Token": short_lived_token},
}
},
skill_ids=("product.email-analysis",),
skill_allowed_tools={
"product.email-analysis": (
"mcp__scoped_data__task_context",
"mcp__scoped_data__list_messages",
"mcp__scoped_data__get_message",
"mcp__scoped_data__record_result",
)
},
allowed_tools=(
"mcp__scoped_data__task_context",
"mcp__scoped_data__list_messages",
"mcp__scoped_data__get_message",
"mcp__scoped_data__record_result",
),
instructions_markdown="# Scoped processor\nUse only the configured MCP tools.\n",
)
agent = ClaudeCodeAgent.from_current_context(
agent_name="email-processor",
workspace_path=workspace_root,
model="sonnet",
allowed_tools=workspace_config.allowed_tools,
permission_mode="acceptEdits",
timeout_seconds=90,
workspace_config=workspace_config,
)
result = await run_claude_code_turn(
agent=agent,
prompt=task_instruction,
kind="regular",
session_store=ClaudeCodeSessionStoreConfig(
implementation="git",
git_repo=claude_session_repo,
local_root=workspace_root / ".claude-session",
tenant=tenant,
project=project,
user_id=user_id,
conversation_id=conversation_id,
agent_name="email-processor",
),
)