DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Mcp Apps — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
M

dsh-mcp-apps

Mcp Apps

Universal MCP Apps Host & UI Plugin for DeepSeek Harness (DSH)

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:katticot/dsh-mcp-apps#34631bfefe2f8c6025e017a804974d88718a31d7
READMECompatibilityVersions
Plugins navAdd plugin dialog

Compatibility and provenance

Mcp Apps is published as dsh-mcp-apps and currently resolves to version 0.2.4. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
9/30/2026

Versions

0.2.4stable
9/30/2026
0.2.3stable
9/28/2026

Related plugins

Loading related plugins…

Latest
0.2.4
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/30/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in ui-customization.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Experimental Agent Team Web Profile@deepseek-ai/dsh-experimental-agent-team-web-profileExperimental Web profile layer for Agent Teams Remote and UI pluginsAb Chartdsh-ab-chartChart rendering tool with the browser card that draws its calls, in one publishable plugin packageWhale Widgetdsh-whale-widgetDeepSeek balance whale widget in the bottom-right corner of the DSH Web interface: balance/today’s usage/peak-off-peak pricing, customizable bubble click sequence (text/balance/today/peak-off-peak/image/random phrases and parallel weighted selection), per-line styles and fonts, floating quick editin

README

dsh-mcp-apps

Renders MCP Apps as sandboxed interactive iframes in DeepSeek Harness (DSH) web chat.


What is this?

MCP servers can return more than text: the MCP Apps extension (SEP-1865) lets a tool result carry a ui:// resource — a small, self-contained web app such as a dashboard, a map, or a form. By default, DSH doesn't know what to do with these and just shows the raw tool result. dsh-mcp-apps is a Cordis 4 plugin for DSH that recognizes these ui:// resources and renders them as live, interactive apps directly inside the chat turn, in a sandboxed iframe. The app can also call back into the host to run more tools (e.g. a "Refresh" button), gated behind an explicit, per-server approval policy.

Features

  • Renders MCP Apps (ui:// resources) as interactive dashboards, charts, maps, and forms right inside DSH chat.
  • Every app runs sandboxed in an iframe with an auto-generated Content Security Policy — no allow-same-origin, no ambient access to the host page.
  • Connects to any MCP server over stdio, SSE, or Streamable HTTP, with automatic reconnect.
  • Apps can call back into host tools (e.g. a "Refresh Data" button), off by default and configurable per server (deny / approve / allow).
  • Rendered apps carry an Interactive App badge and resist DSH's auto-collapse behavior, so they don't fold away when the model finishes streaming.
  • Handles secrets safely: ${VAR} expansion for env vars and headers blocks DSH-internal and secret-shaped variable names unless you explicitly allow them.
  • Registers host-side MCP tools (without the iframe UI) on headless DSH hosts too: the plugin's inject list (tools, connection) has no hard webServer requirement, so tool calls still work even where there's no browser to render into.

Requirements

  • Node.js >= 22
  • DSH (DeepSeek Harness) >= 0.1.7-rc.2 with Cordis ~4.0.4
  • The web profile, for the iframe UI. The plugin's client bundle is injected only when dsh.client.platform is web (see package.json), so its UI does not render in other profiles (e.g. tui, headless) even if the plugin is installed there — but the plugin's host-side tools still register on those profiles (see Features).

The packed plugin has been installed and exercised in a clean DSH 0.1.7-rc.2 web profile, including iframe rendering and delivery of the fixture tool input/result. Other DSH/Cordis combinations remain unverified; see the compatibility record.

Compatibility

DSH checks a plugin's @deepseek-ai/dsh-* peer ranges against its own runtime version (evaluatePluginCompatibility) and refuses to load an incompatible plugin. Match your DSH version to a plugin release:

dsh-mcp-appsRequires DSH
0.2.2 and later>= 0.1.7-rc.2
0.2.1 and earlier0.1.5-rc.x

See docs/dsh-compatibility.md for how the check works and why an exact-pinned peer (no range operator) fails against a later prerelease.

Install

Via the DSH Harness UI

  1. Open DSH Harness and click Plugins in the sidebar.

    Plugins nav

  2. Click + Add plugin, then enter one of the following and click Install:

    • GitHub repository address: https://github.com/katticot/dsh-mcp-apps
    • Package name: dsh-mcp-apps (Official npm registry)
    • Local plugin directory: absolute path to your clone

    Add plugin dialog

Via the CLI

pnpm dlx @deepseek-ai/dsh@0.1.7-rc.2 plugin --profile web add dsh-mcp-apps

This forwards to pnpm inside your DSH profile directory ($DSH_HOME/profiles/web), not your current project — it adds dsh-mcp-apps to that profile's own package.json. Don't run a plain pnpm add dsh-mcp-apps in an unrelated project and expect it to do anything for DSH.

dsh-mcp-apps ships as a DSH bundle (package.json#dsh.bundle): installing it also activates it, whether you install it through the UI or the CLI above. The package's own dsh/cordis.patch.yml inserts a single id: mcp-apps row with an empty server map, so the plugin is loaded (with no servers configured yet) as soon as it's installed — you don't hand-write that insert: yourself, and you shouldn't (see Configure below).

Configure

The plugin is already loaded with an empty servers: {} by the bundle patch above. To add servers, patch that same row from your own profile's patch file (e.g. ~/.dsh/profiles/web/cordis.patch.yml) — a top-level entry (no insert:) that targets the existing id: mcp-apps:

- id: mcp-apps
  config:
    # Required for any `oauth`-enabled server below: this DSH host's own
    # externally reachable base URL, used to build the OAuth redirect_uri.
    # It is never derived from an inbound request (a request's Host header
    # is caller-controlled and cannot be trusted for a value baked into
    # dynamic client registration) — set it explicitly to match however this
    # host is actually reached (localhost, a LAN address, a reverse proxy...).
    externalUrl: 'http://localhost:PORT'
    servers:
      # A remote server speaking Streamable HTTP or SSE
      my-server:
        transport: streamable-http
        url: 'https://your-mcp-server.example.com/mcp'

      # An OAuth-protected remote server, proxied through mcp-remote
      my-oauth-server:
        transport: stdio
        command: npx
        args: [-y, mcp-remote@0.1.37, 'https://your-mcp-server.example.com/mcp']

      # An OAuth-protected remote server, handled natively (no mcp-remote)
      my-native-oauth-server:
        transport: streamable-http
        url: 'https://your-mcp-server.example.com/mcp'
        oauth: true

oauth: true enables native OAuth for sse/streamable-http servers: dynamic client registration (RFC 7591) plus the authorization-code + PKCE flow, with the "Connect Account" button in the plugin's status UI kicking off the browser-facing part. It's remote-transport only — stdio servers keep using the mcp-remote proxy above. oauth requires the top-level externalUrl to be set, and also accepts an object to configure a statically registered client instead of DCR:

      my-static-oauth-server:
        transport: streamable-http
        url: 'https://your-mcp-server.example.com/mcp'
        oauth:
          clientId: 'my-client-id'
          clientSecret: '${OAUTH_CLIENT_SECRET}'
          scopes: [read, write]

Tokens (and any dynamically registered client) are stored per server in ~/.dsh/mcp-apps/oauth/<server-name>.json (plaintext, 0600/0700 permissions — the same access-controlled-not-encrypted trust model this plugin already applies to headers/env secrets).

A patch replaces the targeted row's whole config — it doesn't merge — so restate every key you want (including defaultTimeoutMs and every server, not just the one you're adding or changing).

Do not wrap this in insert:. insert: creates a new row; since the bundle already inserted one id: mcp-apps row for you, inserting a second one gives you two plugin instances both registering the same /api/mcp-apps/* RPC routes, which throws (connection: exact Fetch route "..." is already registered) — see Migrating from 0.2.x if you have an older insert:-based entry lying around.

A fuller example, showing environment expansion, headers, and reverse tool-calls:

- id: mcp-apps
  config:
    defaultTimeoutMs: 30000
    servers:
      # Local stdio subprocess (env values support ${VAR} / ${VAR:-default} expansion)
      local-tool:
        transport: stdio
        command: go
        args: ['run', 'main.go']
        cwd: '/path/to/mcp-server'
        env:
          DATABASE_URL: '${DATABASE_URL}'
        toolCallTimeoutMs: 45000
        allowAppToolCalls: approve

      # Remote SSE server with an auth header
      remote-analytics:
        transport: sse
        url: 'https://mcp.example.com/sse'
        headers:
          Authorization: 'Bearer ${MCP_TOKEN}'
        allowedVars: [MCP_TOKEN]   # secret-shaped vars are blocked unless listed
        reconnectOptions:
          maxRetries: 10

Migrating from 0.2.x

Before 0.2.3, this package wasn't a bundle — you activated it yourself with an - insert: - id: <anything> \n name: dsh-mcp-apps entry in your profile's cordis.patch.yml. Since installing now activates the plugin automatically (via the package's own bundle patch), that old insert: entry is a second, duplicate instance and must be removed:

  1. Delete your old - insert: block that named dsh-mcp-apps (whatever id you gave it) from your profile's cordis.patch.yml.
  2. Replace it with a bare patch entry targeting id: mcp-apps (no insert:), restating your servers config as shown above.
  3. Run npx @deepseek-ai/dsh --profile web --dump-config and confirm exactly one row with name: dsh-mcp-apps appears. Two rows means duplicate servers and a crash on load (see above) — remove whichever insert: you added.

Run & verify

After saving cordis.patch.yml, restart the web profile so the loader applies the plugin entry:

pnpm dlx @deepseek-ai/dsh@0.1.7-rc.2 web

Call a tool on a configured server that returns a ui:// resource. Its tool input and result render in a sandboxed iframe with an Interactive App badge. App callbacks follow that server's allowAppToolCalls policy.


Reverse Tool-Call Security Model

Rendered apps can call back into host tools (e.g. a "Refresh Data" button). That path is locked down by default:

  • Session tokens: every tools/call request from an app must carry a valid AppSessionStore session token, scoped to the tool names and server it was issued for. A missing, expired, or mismatched-server token is rejected before the tool ever runs (src/index.ts).
  • allowAppToolCalls (per server, default false/'deny'): a 'deny' | 'approve' | 'allow' setting (a plain boolean is also accepted as legacy shorthand for false/'allow').
    • 'deny' (default): no tools are exposed for reverse calls from that server's apps.
    • 'allow' / true: reverse calls are permitted without approval.
    • 'approve': each reverse call is routed through ctx.approval (@deepseek-ai/dsh-user-approval) and requires the originating agent to have an open turn (agent.status === 'running'); outside of that window, or if the approval/agent services aren't available, the call fails with unavailable rather than silently allowing or hanging.
  • App-only / model-only visibility: tools marked app-only (isToolVisibilityAppOnly) are never registered with the LLM's tool list, and tools marked model-only (isToolVisibilityModelOnly) are excluded from the set an app is allowed to call back into — each direction is a one-way gate.

Configuration Reference

OptionTypeDefaultDescription
serversRecord<string, ServerConfig>{}Map of server identifiers to transport configs. Names cannot contain __ or end with _.
defaultTimeoutMsnumber30000Fallback timeout in ms for tool calls (must be >= 1).
externalUrlstring—This DSH host's own externally reachable base URL (http(s)://...), used to build the OAuth redirect_uri for any servers.<id>.oauth-enabled server. Required if any server has oauth set; never derived from an inbound request.
servers.<id>.transport'stdio' | 'sse' | 'streamable-http'(Required)Transport mechanism. Only transports defined by the MCP specification are supported: stdio, streamable-http, and sse (deprecated upstream, kept for older servers).
servers.<id>.commandstring—Executable binary path (for stdio).
servers.<id>.argsstring[][]Command arguments (for stdio).
servers.<id>.cwdstring—Working directory (for stdio).
servers.<id>.envRecord<string, string>

${VAR} / ${VAR:-default} expansion only applies to env and headers values — not to url, args, or cwd. $$ is an escaped literal $. A default value cannot itself contain a nested ${...} expansion (e.g. ${MISSING:-${PORT}} is taken literally, not expanded recursively).

Troubleshooting

  • Tools show up twice: running @deepseek-ai/dsh-mcp-client against the same MCP server alongside this plugin registers that server's tools through both plugins, duplicating them. Use one or the other for a given server.
  • ${VAR} isn't expanding: DSH_*-prefixed and secret-shaped (KEY/PASSWORD/SECRET/TOKEN) variable names, plus SSH_AUTH_SOCK/GPG_AGENT_INFO, are blocked by default — add the name to that server's allowedVars.
  • The app's button does nothing: allowAppToolCalls defaults to deny. Set it to approve or allow. Under approve, the call also needs an open, running agent turn to prompt for approval — it fails with unavailable outside of that window.
  • Remote connection rejected: plain http:// is only allowed to a loopback host (localhost, 127.0.0.1, ::1); anything else must be https://.
  • Nothing renders: the client UI only loads under the web profile (dsh.client.platform: "web"). It won't render in tui, headless, or other profiles.
  • Plugin doesn't seem to load / two dsh-mcp-apps rows: the plugin activates itself on install (it's a bundle) with an id: mcp-apps row — your own profile patch should target that same id with a bare top-level entry (no insert:, see Configure). An insert: there creates a second row, which crashes on load (duplicate /api/mcp-apps/* RPC route registration); see Migrating from 0.2.x. Run npx @deepseek-ai/dsh --profile web --patch <file> --dump-config to print the composed config and confirm exactly one name: dsh-mcp-apps row appears.
  • Plugin dsh-mcp-apps@X.Y.Z is incompatible with dsh <version>: peerDependencies {...}: your DSH runtime version doesn't satisfy this plugin's @deepseek-ai/dsh-* peer ranges. Check the compatibility table — dsh-mcp-apps@0.2.2+ requires DSH >= 0.1.7-rc.2; upgrade DSH, or install an older plugin version matched to your DSH release.
  • : check that is set ( fails immediately with a clear error if it isn't) and that it matches an address the browser can actually reach back to for the callback. Click again — a stale authorize attempt's PKCE verifier expires after 10 minutes. Make sure the popup isn't blocked by the browser and that it's able to reach both the authorization server and . If the authorization server rejects the callback with a redirect_uri mismatch, changed since the client was registered — clear to force fresh dynamic client registration and try again.

How it works

The plugin bridges a sandboxed iframe and the DSH host over postMessage-based JSON-RPC, manages a pool of MCP server connections, and normalizes tool names/visibility for the LLM. For the full internals — CSP synthesis, the postMessage handshake, tool fingerprinting, the anti-collapse behavior, and the resize circuit breaker — see docs/how-it-works.md.


Development & Testing

# Install dependencies
pnpm install

# Type-check
pnpm typecheck

# Build host and client bundles (tsdown)
pnpm build

# Run Vitest test suite
pnpm test

./client (the browser-side bundle loaded by DSH's window.__ModuleLoader__) is loader-only: it has no types entry in package.json#exports and cannot be imported directly from Node or a bundler — only the . (host) entry ships type declarations.

See CONTRIBUTING.md for the release process.


License

MIT © katticot

{}
Environment variables, with ${VAR} / ${VAR:-default} expansion (for stdio).
servers.<id>.urlstring—Remote endpoint URL (for sse, streamable-http); must match http(s)://. Plain http:// is only allowed to a loopback host (localhost, 127.0.0.1, ::1) — anything else is rejected at connect time.
servers.<id>.headersRecord<string, string>{}Custom HTTP headers, with the same ${VAR} expansion as env (for sse, streamable-http).
servers.<id>.toolCallTimeoutMsnumber30000Per-server tool call timeout in ms (must be >= 1).
servers.<id>.reconnectOptions{ maxRetries?, initialDelayMs?, maxDelayMs?, backoffFactor? }{5, 1000, 30000, 1.5}Exponential-backoff reconnect tuning, applied on stdio and remote (sse, streamable-http) transports.
servers.<id>.allowAppToolCalls'deny' | 'approve' | 'allow' | booleanfalse ('deny')Reverse tool-call policy — see Reverse Tool-Call Security Model.
servers.<id>.allowedPermissionsstring[][]Allowlist for camera, microphone, and geolocation iframe permissions requested by a resource's UI metadata; all three are denied unless explicitly listed here. Other permission keys pass through unfiltered into the iframe's allow attribute.
servers.<id>.allowedVarsstring[][]Env var names this server may read via ${VAR} expansion (in env or headers) despite being DSH_*-prefixed, secret-shaped (KEY/PASSWORD/SECRET/TOKEN), or an agent socket (SSH_AUTH_SOCK, GPG_AGENT_INFO), which are blocked by default. E.g. allowedVars: ['API_TOKEN'] lets headers: { Authorization: 'Bearer ${API_TOKEN}' } resolve (for stdio, sse, streamable-http).
servers.<id>.maxMessageBytesnumber16777216 (16MB)Maximum size in bytes of a single incoming message (for stdio, sse, streamable-http; must be >= 1). For stdio this is the bytes buffered since the last newline (one JSON-RPC line); for remote transports it caps each individual SSE event or response body (not the total stream lifetime) via a byte-counting wrapper around fetch. Protects against a malicious or misbehaving MCP server exhausting host memory.
servers.<id>.oauthtrue | { clientId?, clientSecret?, scopes?: string[] }—Enables native OAuth for this server (for sse, streamable-http only). true uses dynamic client registration; an object statically configures a client ID/secret/scopes instead. clientSecret is treated as a secret like headers values. Tokens are stored in ~/.dsh/mcp-apps/oauth/<server-name>.json.
OAuth popup doesn't complete / server stuck on "Requires Auth"
externalUrl
oauth/authorize
Connect Account
externalUrl
externalUrl
~/.dsh/mcp-apps/oauth/<server-name>.json