spring-ai-playground

title: Application description: Spring AI Playground architecture - runtime layers, feature modules, data flows, and extension points across Tool Studio, MCP, RAG, and Agentic Chat.

Application

Spring AI Playground is a tool-first Spring Boot application with several UI surfaces layered on top of a shared runtime. The primary packaged experience is a cross-platform desktop app; Docker and source execution are supported as alternative runtimes.

This page explains how the system is organized, how requests flow through it, and where to extend it. It is intended for contributors, integrators, and anyone evaluating how the product is built under the hood.

This is one of six architecture documents that complement each other:

Overview { #overview }

The whole product is one local desktop app built on the idea of “no pass, no run”: a tool is authored, tested in a sandbox against its own values, and only then published to the built-in MCP server where an agent can call it. External MCP servers are reached two ways - Agentic Chat and the MCP Inspector connect to them directly, and selected upstream tools are also composed/proxied onto the built-in server so they reach every consumer (Chat, Inspector, external /mcp clients) wrapped and governed. Agentic Chat composes tools with RAG grounding, served by a local model by default.

flowchart TB
    AUTH["Authored tools<br/>Tool Studio → Local Pass"] -->|"publish"| BUILTIN["Built-in MCP server<br/>/mcp · governed hub"]
    EXT["External MCP servers"] -->|"compose / proxy"| BUILTIN

    BUILTIN --> CHAT["Agentic Chat"]
    BUILTIN --> INSP["MCP Server - Inspector"]
    BUILTIN --> EXTC["External /mcp clients"]

    EXT -.->|"direct"| CHAT
    EXT -.->|"direct"| INSP

Solid arrows are the built-in server path (authored tools + proxied external tools); dotted arrows are direct external connections. The rest of this page expands that flow into runtime layers, feature modules, and data flows.

Design Goals

Spring AI Playground is a Safe Local Execution Layer for AI Agent Tools. A tool a model can invoke is just code running on someone’s machine - so the whole system is shaped to make that code declare what it touches before it touches anything. A tool is a structured spec; the spec runs in a Java-level sandbox on the author’s own host; the sandbox earns the tool a Local Pass against the author’s own test values; only then does the tool reach the built-in MCP server where any client (including the model) can call it. No pass, no run is the product, not a slogan.

That framing drives every other choice on this page:

Runtime Layers

The application is easiest to think about as five layers. Each layer has a well-defined responsibility and a narrow interface with the one above it.

Runtime layer diagram - five stacked layers from Electron launcher down to external runtimes, with the Spring AI integration layer showing the four-advisor ChatClient chain, the built-in and client MCP adapters, the human-in-the-loop approval gates, and encrypted OAuth token storage{ loading=lazy }

Layer 1 - Desktop Launcher (Electron)

Located in electron/. Responsible for packaging, configuration, and process lifecycle - not for any AI behavior.

The launcher is optional. Running the JAR directly or the Docker image skips this layer entirely.

Layer 2 - UI Surfaces

Hand-written Vaadin Flow views under src/main/java/org/springaicommunity/playground/webui/. Not Hilla-generated. Each feature area has a root view (@Route, @SpringComponent, @UIScope) and smaller component views composed inside it.

Package Root view Purpose
webui/home HomeView Landing page with product surfaces
webui/tool ToolStudioView JavaScript tool authoring and test runner
webui/mcp McpServerView, McpServerConnectionView, McpServerConfigView Sidebar (Built-in / Active / Inactive catalog), connection form, Inspector for built-in and external MCP servers
webui/vectorstore VectorStoreView Document upload, chunk inspection, search
webui/chat ChatView Agentic Chat with tools and RAG
webui/observability ObservabilityView Fourteen dashboards (Overview · Tokens & Cost · AI Models · Tool Studio · MCP Servers · MCP Inspector · Vector Database · Agentic Chat · Safety · Host · Ollama · Web Application · Logs · Traces) + Trace Detail / Conversation Thread / Model Pricing Manager dialogs
webui/common/sidebar SidebarFilterBar, CategoryGroupDetails, SidebarItemLayout Shared widgets used by both the MCP Server sidebar and the Tool Studio tool list - search + Categories MultiSelect + Tags MultiSelect, collapsible per-category groups, status dot · name · pills row

Streaming responses (chat, tool execution traces) are pushed to the browser over Vaadin’s WebSocket push.

Layer 3 - Backend Services

Per-feature services under src/main/java/org/springaicommunity/playground/service/, each owning its persistence and runtime concerns. (The observability backend is a separate top-level observability/ package - its UI sits in Layer 2 and its pipeline is documented in AI Agent Observability.)

Package Key services Owns
service/chat ChatService, ChatHistoryService Chat execution, history, tool/RAG composition
service/tool ToolSpecService, ToolCategoryCatalog, ChipListBinding, DefaultToolPresetCatalog, DefaultToolsPreference{Resolver,Service}, ToolActivationCalculator, McpToolDefinition + ToolManifest envelope Tool definitions, preset/preference resolution, draft/exposure state
service/tool/runtime JsToolExecutor, JsRuntimeGlobals, SafeHttpFetch, SafeFs, JsHelperException GraalVM sandbox, fetch SSRF guard, safety.fs, safety.parser.*
service/tool/policy EffectivePolicyResolver, SandboxPostureCalculator Per-tool capability overrides + sandbox risk-level (L0-L5) calculation (distinct from the MCP connection/exposure risk in service/mcp/risk)
service/mcp McpServerInfoService, McpToolCallingManager, LoggingToolCallAdvisor, McpServerHitlToolGate Built-in MCP server metadata, tool-call eventing, and the two human-in-the-loop approval gates (chat-side advisor + server-side elicitation) - see Human-in-the-Loop Approval
service/oauth EncryptedFileOAuth2AuthorizedClientRepository, OAuthTokenEncryptor, McpClientRegistrationRepository, McpOAuth2AuthorizationCodeRequestCustomizer OAuth 2.1 for external MCP connections - encrypted-at-rest token store, dynamic client registration, authorization-code customizer
service/mcp/catalog McpCatalogService, McpCategoryService, McpTagSuggestionService 57-entry preset catalog (49 remote + 8 stdio per OS) - loaded from default-mcp-specs.json and default-mcp-specs-stdio-{mac,linux,windows}.json, plus the 14-row default-mcp-categories.json taxonomy (13 catalog-facing categories + CUSTOM reserved for user-added entries), plus dynamic tag suggestions for the Config form; each entry carries trustSignals + docsAdequate metadata consumed by the risk model
service/mcp/client McpClientService, Mcp*PropertiesService External MCP clients across STDIO / HTTP / SSE
service/mcp/risk McpServerRiskCalculator, McpToolPublishRiskCalculator, McpToolRiskComposer, McpToolPoisoningScanner, McpToolHashLedger, McpCompositionService, McpCompositionShadowingRules, McpExposedToolService, WrappedExternalToolCallback L0-L5 risk scoring for external servers/tools (transport · auth · trust · doc axes + floor rules), tool-description poisoning scan, fingerprint ledger for change detection, and tool composition that re-exposes upstream tools on the built-in server with alias / description / HITL overrides - see MCP Server Safety
service/util SecretMasking, EnvVarResolver Resolve ${ENV_VAR} placeholders against the OS env; sweep connection-error notifications + per-call logs to replace any resolved secret value with ***
service/vectorstore VectorStoreService, VectorStoreDocumentService Tika ingestion, chunking, embedding, search
service/identity DeviceIdProvider, UserIdentityService, MdcIdentityFilter (in config) Stable per-device id - salted SHA-256 of the OS machine id, persisted to identity/installation.json; injected into MDC as userId / sessionId for log + trace correlation (see Observability → Device-based identity)
observability (top-level) ObservabilityCollector, ObservabilityRingBuffer, ObservabilityTimeSeries, ModelPricingService Micrometer ObservationHandler pipeline + ring buffer / time series / JSON persistence that backs the Observability dashboards - a sibling of service/, shown here because it is runtime backend. Full design: AI Agent Observability

Persistence is pluggable via PersistenceServiceInterface and coordinated by SpringAiPlaygroundPersistenceManager on startup / shutdown. The default writes JSON files under the user home directory.

Layer 4 - Spring AI Integration

Thin adapter layer configured in SpringAiPlaygroundApplication and related Spring @Configuration classes.

Layer 5 - External Runtimes

Everything outside the JVM:

Feature Modules

flowchart LR
    TS[Tool Studio]
    MCPV[MCP Server]
    BUILTIN[Built-in MCP Server]
    EXT[External MCP Servers]
    VDB[Vector Database]
    CHAT[Agentic Chat]
    HITL[Human-in-the-Loop<br/>approval gate]
    OBS[Observability]

    TS -- "publishes tools" --> BUILTIN
    MCPV -. "inspects · tests" .-> BUILTIN
    MCPV -. "registers · tests" .-> EXT
    BUILTIN -- "exposes tools" --> CHAT
    EXT -- "exposes tools" --> CHAT
    VDB -- "retrieves grounded context" --> CHAT
    CHAT -- "gates each call" --> HITL
    BUILTIN -- "gates external callers" --> HITL
    CHAT -. "spans" .-> OBS
    BUILTIN -. "spans" .-> OBS
    EXT -. "spans" .-> OBS
    VDB -. "spans" .-> OBS
    HITL -. "spans" .-> OBS

The main surfaces are connected parts of one workflow, not isolated demos:

Key Data Flows

Flow 1 - Tool authoring and publication

A tool defined in Tool Studio is a FunctionToolCallback whose executor delegates to the GraalVM JavaScript sandbox. Publishing registers the callback with the built-in McpSyncServer so external MCP clients (Claude Desktop, Claude Code, etc.) can call it.

flowchart TB
    UI["Tool Studio view<br/>code · params · vars"]
    SVC["ToolSpecService<br/>update"]
    CB["FunctionToolCallback"]
    EXE["JsToolExecutor<br/>sandbox run"]
    POLY["GraalVM context<br/>allowlist · limits"]
    MCPSRV["addTool<br/>(McpSyncServer)"]
    MCPEP["Built-in /mcp<br/>Streamable HTTP"]

    UI --> SVC
    SVC --> CB
    CB -. "test run" .-> EXE
    EXE --> POLY
    SVC -- "publish" --> MCPSRV
    MCPSRV --> MCPEP

Sandbox policy is configurable under spring.ai.playground.tool-studio.js-sandbox. The defaults are deny-first: raw network I/O, file I/O, native access, and thread creation are all blocked at the Java level. A deny-classes list (System, Runtime, Process, Class, reflect, invoke, Thread, ClassLoader, ServiceLoader, spi) is evaluated before any allow-class match, so deny always wins. The allow-classes are limited to pure-compute packages (java.lang/math/time/util/text.*). Tools talk to the outside world through built-in helpers - fetch (four-layer SSRF guard, strict egress), safety.fs (rooted at tool-studio.fs.base-path), and safety.parser.{html,yaml,csv,xml} - and a tool that genuinely needs more opens specific capabilities through per-tool overrides on its SandboxOverrides block (addAllowClasses, hostsAllow, networkMode, fileRead/fileWrite, fsBasePath), which raise its visible risk level (L0-L5) computed by SandboxPostureCalculator. See Tool Studio → Sandbox & Capabilities for the full override shape, egress mode behavior, and risk-level rules.

Publishing has two states. A new or unverified tool is a Draft - it lives in Tool Studio and is not registered with the built-in MCP server. A Local Pass (a successful test run with the declared test values) flips the McpToolDefinition exposure flag and ToolActivationCalculator registers the callback with McpSyncServer. Which built-in tools are Local-Passed (active) on boot is decided by DefaultToolPresetCatalog + DefaultToolsPreferenceResolver, configured at setup (the launcher’s Default MCP Tools card or a CLI override). Tool Studio’s Built-in MCP Server Native Tools drawer then selects which Local-Passed tools the server exposes.

Flow 2 - External MCP server connection

McpClientService is transport-agnostic. A dedicated Mcp*PropertiesService knows how to build a transport from the connection JSON for each McpTransportType.

flowchart TB
    FORM["Connection view<br/>transport + JSON"]
    START["McpClientService<br/>start client"]
    MAPTransport type
    STDIO["STDIO<br/>spawn process"]
    HTTP["Streamable HTTP<br/>transport"]
    SSE["SSE<br/>transport"]
    INIT["McpClient<br/>initialize handshake"]
    REG["Connected clients<br/>serverInfo → ops"]
    INSP["Inspector view<br/>list · test tools"]
    RISK["Risk calculator<br/>live risk chip"]

    FORM -.->|live preview| RISK
    FORM --> START
    START --> MAP
    MAP --> STDIO & HTTP & SSE
    STDIO & HTTP & SSE --> INIT
    INIT --> REG
    REG --> INSP

Once registered, the same connection becomes available as a tool source in Agentic Chat. While the form is open, McpServerRiskCalculator scores the in-progress connection and renders a live risk chip beside the transport selector (see MCP Server Safety).

Flow 2b - Exposing external tools (composition)

Beyond inspecting a connection, the operator can re-expose selected upstream tools on the built-in MCP server through the Composed Tools drawer. McpCompositionService persists the selection (exposed alias, description override, per-tool HITL, max-risk cap); on enable, McpCompositionShadowingRules validates the set (alias collisions, cross-server references), McpToolRiskComposer combines server + tool risk (lowered one band when HITL is set), and McpExposedToolService wraps each chosen tool in a WrappedExternalToolCallback registered with McpSyncServer - so it joins the Tool Studio tools published at /mcp.

flowchart LR
    DRAWER["Composed Tools drawer<br/>select · alias · HITL · cap"]
    SVC["Composition service<br/>persist selection"]
    RULES{"Shadowing rules<br/>R1 · R2 · R3"}
    WRAP["Wrapped tool<br/>alias + risk MDC"]
    BUILTIN["Built-in /mcp<br/>(McpSyncServer)"]

    DRAWER --> SVC --> RULES
    RULES -->|ok| WRAP --> BUILTIN
    RULES -->|violation| SVC

Flow 3 - Document ingestion and RAG

flowchart LR
    UP["Upload<br/>PDF · DOCX · HTML"]
    TIKA["Tika reader"]
    SPLIT["Text splitter<br/>chunk 800 · min 350"]
    TAG["Tag with docInfoId"]
    EMBED["Embedding model"]
    STORE["Vector store<br/>add"]
    DI["Document info<br/>metadata · lazy"]

    UP --> TIKA --> SPLIT --> TAG --> EMBED --> STORE
    STORE --> DI

Searches go through VectorStoreService.search(query, filterExpression) which builds a SearchRequest with similarity threshold 0.6 and top-K 10 by default. The docInfoId metadata makes it possible to scope retrieval to specific documents in Chat.

Flow 4 - Chat advisor chain (memory + RAG)

Every chat request passes through the ChatClient advisor chain before it reaches the model. The chain is assembled from all Advisor beans injected as an array (defaultAdvisors(Advisor[])) and ordered by each advisor’s getOrder() - MessageChatMemoryAdvisor, SpringAiPlaygroundRagAdvisor, SimpleLoggerAdvisor, and LoggingToolCallAdvisor (which owns the tool-calling loop and runs the human-in-the-loop gate; see Flow 5).

sequenceDiagram
    autonumber
    participant CS as ChatService
    participant CCL as ChatClient
    participant MEM as Memory advisor
    participant CMEM as ChatMemory<br/>(MessageWindow, last 10)
    participant RAG as RAG advisor (ours)
    participant RAA as Spring AI RAG
    participant VSS as VectorStoreService
    participant LOG as SimpleLoggerAdvisor
    participant MODEL as ChatModel

    CS->>CCL: prompt().user(..).advisors(conversationId, ragFilter)
    CCL->>MEM: before(request)
    MEM->>CMEM: read prior messages
    CMEM-->>MEM: last-N window
    MEM-->>CCL: request + attached history
    alt ragFilterExpression present
        CCL->>RAG: before(request)
        RAG->>RAA: build with filter-bound retriever
        RAA->>VSS: search(query, filter)
        VSS-->>RAA: documents (threshold 0.6, top-K 10)
        RAA-->>RAG: DOCUMENT_CONTEXT populated
        RAG-->>CCL: request + grounded context
    end
    CCL->>LOG: before(request)
    LOG-->>CCL: (logged)
    CCL->>MODEL: send prompt

RAG only runs when the user selected at least one document - otherwise SpringAiPlaygroundRagAdvisor short-circuits and the chain moves on. Retrieved documents are carried in the request’s DOCUMENT_CONTEXT so the UI can render them alongside the final answer.

Flow 5 - Chat with MCP tools

Tool callbacks come from MCP clients, not from code you compile in. When a user picks one or more MCP servers in Chat, McpClientService hands back a ToolCallbackProvider for each live connection (built-in or external). The model sees their tools as ordinary function tools; McpToolCallingManager intercepts every call so the UI can show it.

sequenceDiagram
    autonumber
    participant CCV as Chat view
    participant MCS as MCP client
    participant PROV as Sync · Async<br/>ToolCallbackProvider
    participant CCL as ChatClient
    participant MODEL as ChatModel
    participant TCM as McpToolCallingManager
    participant CB as Sync · Async<br/>McpToolCallback
    participant MCP as MCP Server<br/>(built-in · STDIO · HTTP · SSE)
    participant UI as UI stream

    CCV->>MCS: buildToolCallbackProviders(selected servers)
    MCS-->>CCV: ToolCallbackProvider per server
    CCV->>PROV: getToolCallbacks()
    PROV-->>CCV: ToolCallback list
    CCV->>CCL: .toolCallbacks(callbacks) + toolContext(MCP_PROCESS_MESSAGE_CONSUMER)
    CCL->>MODEL: prompt with tool definitions
    MODEL-->>CCL: assistant message with tool_calls
    CCL->>TCM: executeToolCalls(prompt, response)
    TCM-->>UI: push user / tool-call events
    TCM->>CB: invoke callback
    CB->>MCP: callTool(name, args) over transport
    MCP-->>CB: CallToolResult
    CB-->>TCM: tool response message
    TCM-->>UI: push tool-result event
    TCM-->>CCL: conversation with tool output appended
    CCL->>MODEL: follow-up request
    MODEL-->>CCL: final assistant text

The same path handles the Built-in MCP Server (loopback Streamable HTTP at /mcp) and external servers (STDIO, Streamable HTTP, SSE) - only the transport differs.

Flow 6 - Agentic Chat

Agentic Chat is the compose step: it drives Flow 4 and Flow 5 in one streaming request, letting the model decide how many tool-call rounds to run before it produces the final answer.

sequenceDiagram
    autonumber
    participant U as User
    participant CCV as Chat view
    participant CS as ChatService
    participant CCL as ChatClient
    participant ADV as Advisor chain<br/>(Flow 4)
    participant VSS as VectorStoreService
    participant MODEL as ChatModel
    participant TCM as McpToolCallingManager<br/>(Flow 5)
    participant MCP as MCP Server(s)
    participant UI as UI stream

    U->>CCV: prompt + selected docs + selected MCP servers
    CCV->>CS: stream(prompt, filter, toolCallbacks, consumers)
    CS->>CCL: prompt().user(..).toolCallbacks(..).advisors(..)
    CCL->>ADV: before(request)
    ADV->>VSS: RAG search (if filter present)
    VSS-->>ADV: grounded documents
    ADV-->>CCL: request with memory + documents
    loop until model stops calling tools
        CCL->>MODEL: streaming request (tools attached)
        MODEL-->>UI: thinking · partial text
        MODEL-->>CCL: tool_calls (if any)
        CCL->>TCM: executeToolCalls()
        TCM->>MCP: callTool over transport
        MCP-->>TCM: CallToolResult
        TCM-->>UI: tool call · result events
        TCM-->>CCL: conversation with tool output
    end
    MODEL-->>UI: final answer stream
    UI-->>CCV: render (retrieved docs · tool calls · results · thinking · answer)

Retrieved documents, every tool call, every tool result, and any reasoning trace are all surfaced in the UI - the agent’s path from question to answer is explicit rather than hidden.

Safe Tool Spec

The Safe Tool Specification is the on-disk contract that makes the “Safe Local Execution Layer” claim concrete. Every tool - bundled in the playground’s catalog (src/main/resources/tool/default-tool-specs-*.json) or authored in Tool Studio (saved under ~/spring-ai-playground/tool/save/) - serializes into one JSON document with three coupled concerns: identity the model sees, code the runtime executes, and safety posture the sandbox will enforce. The name distinguishes it from generic tool specs (MCP’s tools/list schema, OpenAI function calling) that describe an interface but say nothing about what is safe to let an agent run.

The spec maps directly onto the four parts of the product positioning:

Author intent → enforced posture

A safe tool spec carries two safety-related blocks that look similar but serve opposite directions:

The split exists so that (1) the editable intent and the enforced posture cannot drift, (2) verifying a foreign spec’s safety properties only needs reading toolSafety, and (3) the audit log captures what was actually enforced, not what the author asked for.

For the full document grammar - every field, JSON Schema, resolution algorithm, network mode behavioral table, Risk Level computation, versioning policy, validation error model, and canonical examples - see Safe Tool Specification 1.0. For the resolver internals and threat-to-layer mapping, see AI Agent Tool Safety. For the Tool Studio UI form that writes this JSON, see Tool Studio → Sandbox & Capabilities.

Sandbox Safety

Tool Studio is the only part of the system that runs user-authored code. The implementation models safety as three independent layers - an always-on Java-level sandbox, a per-tool override surface with a visible risk badge, and a transport-level security layer in front of the MCP endpoint.

For the system-level reference (three-layer diagram, policy resolution, per-execution enforcement, threat-to-layer mapping, known limitations, and the next-pass HITL design), see → AI Agent Tool Safety Architecture.

For the user-facing surface (override fields, Risk Level rules, SSRF four-layer steps), see → Tool Studio → Safety and Tool Studio → Sandbox & Capabilities.

Configuration and Profiles

Location Purpose
src/main/resources/application.yaml Base defaults, profile declarations
src/main/resources/tool/default-tool-specs*.json Built-in tools shipped with the app (split by bundle: default-tool-specs.json, -builtin.json, -builtin-helpers.json, -builtin-fs.json, -network.json, -kr.json)
electron/resources/default-application.yaml Desktop launcher’s default config template
electron/launcher-config.js Provider starter templates (Ollama, OpenAI, OpenAI-compatible)

Runtime selection happens through Spring profiles (ollama, openai) combined with user configuration written by the launcher. The same JAR can target any supported provider - no rebuild required.

Persistence

SpringAiPlaygroundPersistenceManager hooks into Spring’s lifecycle and delegates to per-feature persistence services:

State is serialized as JSON under the user home directory. SimpleVectorStore itself is volatile - vectors are recomputed on restart when the default store is in use. Swapping in a durable vector store (pgvector, Weaviate) removes that constraint.

Extensibility Points

Extension Where
New model provider Spring profile + application.yaml + launcher template
New vector store Standard Spring AI VectorStore bean override
New MCP transport Add an McpClientPropertiesService<T> implementation and register it against McpTransportType
New tool Tool Studio (runtime, no rebuild) or a Spring bean exposing a ToolCallback
Custom advisor Register an additional Advisor bean; picked up by ChatClient builder
Custom persistence Implement PersistenceServiceInterface

Why This Shape

The five-layer model is deliberate. Each capability has a dedicated runtime area, but the user-facing flows compose those capabilities rather than hiding them behind a single opaque screen. That is what makes the app useful as a validation environment: every boundary - sandbox, MCP transport, retrieval, tool execution - is visible and testable in isolation before it is combined in Chat.

Further Reading