Workflow & Automation

dsh-flowus

BlkSword/dsh-flowus · ★ 0

BlkSword

FlowUs for DeepSeek Harness: browser OAuth sign-in against the official FlowUs MCP server, its tools registered as native agent tools, a /flowus command, and a Settings -> FlowUs page.

License
Apache-2.0
Added
2026-10-30
Maintenance
active
Install kind
npm
npm
@blksword/dsh-flowus
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:39:05.301Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add '@blksword/dsh-flowus@0.1.1'

从 npm 安装预构建包

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. Scope
  2. Background
  3. Requirements
  4. Installation
  5. From npm
  6. From GitHub
  7. From a local checkout
  8. After installation
  9. Compatibility
  10. First authorization
  11. Commands
  12. Settings page
  13. Configuration
  14. Credentials and security
  15. Implementation
  16. Development and testing
  17. Verifying against another runtime
  18. Releasing
  19. Troubleshooting
  20. Known limitations
  21. Repository layout
  22. License

English | 中文

A FlowUs integration plugin for DeepSeek Harness (DSH). The plugin performs OAuth 2.1 authorization against the official FlowUs MCP server (https://mcp.flowus.cn/message) entirely within the host process — RFC 9728 metadata discovery, RFC 7591 dynamic client registration and PKCE S256 — registers the tools published by that server as native agent tools, and provides a /flowus command together with a settings-panel page. The authorization flow does not use a static token.

Scope

  • Browser authorization. An OAuth 2.1 authorization-code flow against the official FlowUs MCP server, with a loopback callback listener on 127.0.0.1. Access and refresh tokens are stored in a plugin-private state file and renewed automatically.
  • Native tools. Every tool published by the FlowUs MCP server is registered with the DSH tool registry under the same naming convention as the stock MCP bridge, namely mcp__flowus__<tool>.
  • /flowus command. Status reporting, authorization, sign-out, tool listing, direct invocation, reconnection and help.
  • Settings page. A dedicated FlowUs page in the settings panel: authorization and connection state, the server version, the tool catalogue (searchable by name or description, each entry labelled read-only or write), and the authorization, reconnection, sign-out and refresh actions. The credential path and the authorization URL can be copied with one click, the page polls while a browser login is pending, and it refreshes immediately when the tab regains focus.
  • System-prompt section. The server instructions and the current tool catalogue are contributed to the system prompt, with explicit guidance when the workspace is not yet authorized.

Background

The DSH package @deepseek-ai/dsh-mcp-client supports stdio and streamable-http with static request headers, but not MCP OAuth. The official FlowUs MCP server uses OAuth 2.1 and requires a client that cannot complete the authorization flow to report that condition rather than fall back to a static token. This plugin implements the OAuth client flow and bridges the discovered tools onto ctx.tools.

Requirements

Item Requirement
DSH 0.2.0-rc.2, 0.2.1-alpha.1 or 0.2.1-alpha.2 (see Compatibility)
Node.js >= 22

Installation

From npm

dsh plugin --profile web add @blksword/dsh-flowus@latest

dsh plugin add performs the following steps:

  1. installs the package through pnpm;
  2. reads the dsh.bundle.patch declaration in the package and appends @blksword/dsh-flowus to dsh.profile.bundles, so no manual edit of package.json is required;
  3. resolves the @deepseek-ai/* implementations provided by the DSH installation through peer dependencies.

pnpm 11 applies minimumReleaseAge, which defaults to 1440 minutes: a version published less than 24 hours ago is skipped when @latest is resolved, the resolution falls back to the previous version, and the exit code still reports success. To install a recent release, name the exact version:

dsh plugin --profile web add @blksword/dsh-flowus@0.1.1

From GitHub

The repository is public, so the same package can be installed without a registry:

dsh plugin --profile web add github:BlkSword/dsh-flowus

This path has been verified end to end: pnpm resolves the git dependency, the bundle entry is written to dsh.profile.bundles, and the loader composes and imports the plugin row (dsh --profile <name> --dump-config and --dump-config-schema both succeed). For a reproducible installation, pin a commit with github:BlkSword/dsh-flowus#<commit-sha>; subsequent updates are retrieved with dsh plugin --profile web update @blksword/dsh-flowus.

From a local checkout

# 1. Pack into the profile directory
npm pack --pack-destination "$env:USERPROFILE\.dsh\profiles\web"

# 2. Install from the profile directory (relative specifier)
dsh plugin --profile web add "file:./blksword-dsh-flowus-0.1.1.tgz"

Windows note: pnpm interprets absolute link:D:/... and file:D:/... specifiers as relative paths and creates an invalid link pointing at ...\profiles\web\D:\.... Pack the tarball into the profile directory first, then use the relative file: specifier shown above.

After installation

Restart dsh web and reload the browser page. The client entry graph (window.__DSH_BOOT__) is composed at process start, and with the default dsh-hmr configuration (root: []) only explicit configuration watches remain active, so a newly added plugin is not hot-loaded into a running process. Confirm the result with /flowus status or through Settings → FlowUs.

Compatibility

This release supports DSH 0.2.0-rc.2, 0.2.1-alpha.1 and 0.2.1-alpha.2, and no other versions. The peer dependencies and the compatibility record name exactly those three releases.

DSH release @deepseek-ai/cordis @deepseek-ai/schemastery
0.2.0-rc.2 ~4.0.4 ~3.18.4
0.2.1-alpha.1 ~4.0.5-alpha.1 ~3.18.5-alpha.1
0.2.1-alpha.2 ~4.0.5-alpha.1 ~3.18.5-alpha.1

The @deepseek-ai/dsh-* peer dependencies use one exact-version disjunction, 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2, while @deepseek-ai/cordis and @deepseek-ai/schemastery use the paired ranges ~4.0.4 || ~4.0.5-alpha.1 and ~3.18.4 || ~3.18.5-alpha.1. Every @deepseek-ai/* package is provided by the active dsh profile and must not be added as a separate dependency.

The matrix above was established by measurement: the offline suite and the live smoke tests were executed against the packages shipped by each of the three runtimes, with no failures, and the named-export sets used by the plugin are identical across all three. The alpha range clauses are required rather than cosmetic: a prerelease version satisfies a range only when a comparator carries a prerelease identifier on the same major.minor.patch, so ~4.0.4 does not accept 4.0.5-alpha.1, and 0.2.0-rc.2 does not accept 0.2.1-alpha.1.

Newer releases are measured but not claimed. @deepseek-ai/dsh@0.2.1-alpha.3 and alpha.4 are published; this plugin does not declare support yet:

  • The host half passes on alpha.3. Against a complete alpha.3 runtime (609 packages, peer closure resolved) all 164 offline tests pass exactly as on 0.2.0-rc.2 (161 passed, 0 failed, 3 live skipped), and the alpha.3 pairings (cordis@4.0.5-alpha.2, schemastery@3.18.5-alpha.2, dsh-tools@0.2.1-alpha.3) already fall inside the declared ~ and exact-disjunction ranges, so no declaration needs to change.
  • The client half is unverified. The alpha line no longer ships @deepseek-ai/dsh-client-ui-primitives as a runtime dependency package (it is absent from node_modules\@deepseek-ai), while the settings-page classic script requires that module id from the shell's table; the id still appears in the alpha.3 shell bundle, so the outcome is open. Claiming the release requires one real settings-page load on alpha.3.
  • @deepseek-ai/dsh-atomic-write joined the peer set as an optional peer: when present it supplies the atomic state-file write and the cross-process writer lock.

First authorization

/flowus login
  1. The plugin opens a one-shot callback listener on 127.0.0.1:42813/callback.
  2. The default browser is opened on the FlowUs authorization page. If it cannot be opened automatically, the full authorization URL is printed by the command and shown on the settings page.
  3. After the user approves the request, the browser is redirected to the callback address.
  4. The plugin exchanges the authorization code together with the PKCE verifier for a token set, then connects to the MCP server, discovers the tools and registers them.
  5. The command reports the number of registered tools.

Once authorization has completed, FlowUs tasks can be requested from the agent in plain language.

Commands

Command Effect
/flowus or /flowus status Authorization state, connection state, credential-file path and the registered tool list
/flowus login Browser authorization followed by a tool synchronization
/flowus logout Deletes the tokens while keeping the dynamic client registration, which avoids the registration rate limit
/flowus tools Lists the FlowUs tools registered for the model
/flowus call <tool> [json] Invokes one tool directly, for example /flowus call search_pages {"query":"weekly report"}
/flowus reload Reconnects and re-synchronizes the tool list
/flowus help Usage

Settings page

The plugin contributes a dedicated page to the settings panel through the settings.section slot (id: flowus). The page reports state and performs the authorization-related operations:

  • State indicator. Not authorized, waiting for the browser, authorized but not connected, or connected (with the tool count), rendered with StateDot and Tag.
  • State details. MCP endpoint, tool namespace, credential-file path, granted scope, token expiry, whether a refresh token is stored, last synchronization time and the most recent error.
  • Actions. Sign in (when unauthorized), re-authorize (when authorized), cancel (while an authorization is in progress), reconnect, sign out and refresh.
  • Tool list. The mcp__flowus__* tools currently registered for the model, with their short descriptions.

Page data is read from the host route GET /flowus/api/status; the buttons post to /flowus/api/login, /flowus/api/login-cancel, /flowus/api/logout and /flowus/api/reload respectively. Every response carries the same status snapshot ({ ok, snapshot }), so a single rendering path covers all operations.

These routes behave like every other DSH web surface: each request first passes the connection service's trust fence (Host/Origin validation and the login-token cookie). An unauthenticated request receives 401 and can neither read state nor start an authorization.

Authorization uses a start-then-poll pattern: POST /flowus/api/login returns immediately while the browser flow runs in the background, bounded by loginTimeoutMs; the page refreshes its status every 1.5 seconds while waiting, so no HTTP request is held open for an extended period.

Configuration

Configuration is supplied by a profile patch layer ($DSH_HOME/profiles/web/cordis.patch.yml):

- id: flowus
  config:
    callbackPort: 42813
    toolCallTimeoutMs: 60000
Field Default Meaning
serverUrl https://mcp.flowus.cn/message FlowUs MCP resource URL
serverName flowus Tool namespace (mcp__<serverName>__*)
scope all Requested OAuth scope (FlowUs publishes all)
autoConnect true Connect and publish tools at startup when a usable token exists
failOnStartupError false Whether a failed initial connection or tool synchronization fails plugin activation
statePath empty → <DSH_HOME>/flowus/oauth.json Credential and registration state file
clientName DeepSeek Harness Client name shown on the authorization page
callbackHost 127.0.0.1 Callback listener host
callbackPort 42813 Callback port; a fixed port allows one client registration to be reused
callbackPath /callback Callback path
loginTimeoutMs 300000 Upper bound for waiting on the browser
openBrowser true Whether to open the default browser automatically
toolCallTimeoutMs 60000 Timeout for a single tool call
maxTools 128 Upper bound on published tools (0 disables the bound)
maxResultChars 32000 Character ceiling for one tool result forwarded to the model; over it the head and tail survive with a stated omission (0 disables the bound)
deferToolLoading false Publish the FlowUs tools as deferred: on a route that supports it the schemas are declared with defer_loading instead of entering the always-on context; a route without support ignores the flag
maxInstructionChars 32000 Character ceiling for the server's own instructions before they enter the system prompt; the rest is truncated and reported in the log (0 disables the bound)
promptSection true Whether to contribute the FlowUs guidance and server instructions to the system prompt

Credentials and security

  • The state file is located at %DSH_HOME%\flowus\oauth.json (by default C:\Users\<user>\.dsh\flowus\oauth.json) and contains the dynamically registered client_id, the token set, the PKCE verifier and the discovery cache. Writes prefer the harness primitives (@deepseek-ai/dsh-atomic-write: random-suffix sibling, wx exclusive create and rename, with its file lock serializing a cross-process read-modify-write; the peer is declared optional, and the store falls back to an equivalent local implementation), and set permissions to 0600 where the platform supports it. Logout also prunes leftover <file>.*.tmp and <file>.corrupt siblings.
  • The client is a public client: token_endpoint_auth_method = "none" together with PKCE S256, and no client secret exists. The refresh token is the only long-lived credential.
  • logout removes the tokens and the verifier while retaining the dynamic client registration. The registration endpoint is rate-limited (an observed X-RateLimit-Limit of 3), so reusing the registration avoids throttling on subsequent sign-ins.
  • Network endpoints in use: mcp.flowus.cn (resource metadata and the MCP data plane) and api.flowus.cn (authorization, token and registration).
  • To revoke access, revoke the application authorization on the FlowUs side and then run /flowus logout locally.

Implementation

/flowus login
  ├─ open a one-shot listener on 127.0.0.1:42813/callback (state generated first)
  ├─ auth(provider, { serverUrl, scope })          ← driven by the MCP SDK
  │    ├─ RFC 9728 discovery  /.well-known/oauth-protected-resource
  │    ├─ RFC 8414 discovery  /.well-known/oauth-authorization-server
  │    ├─ RFC 7591 dynamic client registration when nothing is stored
  │    │  (loopback redirect, native application type)
  │    └─ build the PKCE(S256) authorization URL → redirectToAuthorization → browser
  ├─ browser approval → redirect carries ?code&state (a state mismatch is rejected)
  ├─ auth(provider, { serverUrl, authorizationCode, iss })
  │    └─ exchange for the token set (access + refresh) and persist it
  └─ MCP initialize → tools/list → register each tool with DSH (mcp__flowus__*)
  • Token renewal. On a runtime 401 the SDK refreshes the token with the stored refresh token and retries once; if the retry still fails, the plugin reports an explicit "re-authorization required" error. The plugin records the acquisition time and lifetime of the token set and refreshes before connecting when the access token is already stale, so the first request does not have to fail with 401.
  • Tool-list changes. notifications/tools/list_changed triggers a re-synchronization: the plugin adopts the new list and re-registers the whole generation, keeping the previous one when a re-registration fails.
  • Session self-healing. When the server has forgotten the session (JSON-RPC -32001, or a 404 from the session endpoint), the plugin reconnects once, re-registers the tool list and replays the call in flight, so no manual /flowus reload is required.
  • Transient-failure backoff. The nginx in front of mcp.flowus.cn answers short 503 windows (measured: every MCP endpoint 503, healthy again eight seconds later). A gateway rejection is replayed with bounded exponential backoff and jitter (three retries, 500 ms base, 8 s cap); an ambiguous failure (500/429/reset) is replayed only for a read-only tool, so a write is never applied twice by the plugin's own retry. A transient sync failure re-syncs on its own after 5 s, 15 s and 60 s, matching the harness's own MCP bridge.
  • Failure descriptions are sanitized. The SDK puts the whole response body in its error message, so everything the plugin surfaces — the status snapshot, the settings page, /flowus status, the tool error the model reads — is one short sentence: a known status is named, markup is stripped, whitespace collapsed and the text capped at 200 characters; the raw gateway page only reaches the log. The snapshot also carries lastErrorKind, so the page and /flowus status can say the failure is temporary and being retried.
  • Result bound and call cards. One result is truncated to maxResultChars before it reaches the model: the head and tail survive with the dropped character count stated, structured content is bounded the same way, and an oversized inline payload (a base64 image or blob) is replaced by its size in the stored raw blocks. Each tool also supplies a call card whose kind separates reads and searches from writes and whose title names the subject of the call.
  • Serialized invocation. The FlowUs server permits only one in-flight tool call per MCP session; a concurrent call is answered with JSON-RPC -32002. The plugin therefore executes calls through a single serial queue and additionally retries a concurrency rejection with bounded backoff, so parallel read-only scheduling by the model does not fail (measured: five concurrent calls, all successful).
  • Naming conflicts. If an mcp__flowus__* name is already owned by another plugin, the entire synchronization is refused and no partial generation is published.
  • Callback validation. The callback listener accepts GET only and requires exactly one state that matches its own (compared in constant time). A request that fails either check is answered and then ignored — it never settles the login — so no local process or page can cancel an authorization in progress. The listener is bound to loopback only (RFC 8252 §7.3), and server-supplied error text reaches the browser escaped and truncated.
  • Credential binding. Stored credentials record the resource URL they were issued for. When the configured serverUrl points at another resource, the plugin refuses to present the token and asks for re-authorization instead of handing the FlowUs token to a third-party endpoint.
  • Settings page. The host half registers /flowus/api/* through ctx.webServer.register; the client half is a window.__ModuleLoader__ classic script (lib/client.js, with no build step) registered into the settings.section slot. The two halves communicate through fetch and JSON snapshots; no custom RPC protocol is introduced.

Development and testing

Local development installs from the lockfile and links one DSH runtime in (node_modules is not tracked):

npm ci --ignore-scripts --legacy-peer-deps                    # install from the lockfile (includes typescript / @types/node)
npm install --no-save --no-package-lock --ignore-scripts --legacy-peer-deps @deepseek-ai/dsh@0.2.0-rc.2
npm run link-runtime                                          # link each package to that runtime

npm run check                                                 # typecheck + offline suite: 180 tests, 177 passed, 3 live tests skipped
npm audit                                                     # dependency audit, which needs the lockfile
$env:DSH_FLOWUS_LIVE=1; node --test tests/flowus-live.test.js  # live smoke tests: 3

Link layout and order. scripts/link-runtime-packages.mjs links every @deepseek-ai/* package individually (a junction on Windows) instead of linking the whole scope directory. The difference is measurable: npm install deletes the individual links but keeps the packages they point at, while a whole-scope junction lets npm walk through the link and delete the packages inside the runtime — which is how one DSH installation was destroyed. The order still holds: run every npm command first, link afterwards, and re-run npm run link-runtime after any npm/pnpm command that follows.

Test coverage:

File Coverage
tests/tool-names.test.js Tool-naming contract (identical to the stock MCP bridge)
tests/command-grammar.test.js /flowus grammar
tests/store.test.js Atomic writes for the state file (harness primitives and the local fallback), cross-instance serialization, corruption quarantine and leftover pruning
tests/oauth-provider.test.js OAuthClientProvider semantics, the issuer binding (a credential is never presented to another authorization server) and state invalidation scopes
tests/callback-server.test.js Callback validation (state, code, error, port fallback, abort)
tests/login.test.js The two-leg exchange with an injected authImpl (no network): success, already-authorized short circuit, no authorization URL, no exchange, browser disabled, cancellation, timeout, stale-redirect invalidation
tests/browser.test.js Shell-free browser launch and candidate fallback
tests/tool-bridge.test.js MCP JSON Schema to DSH parameter DSL, plus the output contract checked with DSH's own schema compiler
tests/text-bound.test.js Truncation edges for results and instructions: head and tail kept, omission stated, no split surrogate pair, 0 disables the bound
tests/http-api.test.js Settings-page host API: trust fence, method guard, { ok, snapshot } envelope, error path
tests/client-bundle.test.js Browser half: __ModuleLoader__ registration contract, dictionary key parity, slot registration, host-route path cross-check, view-model branches and one full render
tests/mcp-session.test.js Result normalization, concurrency-rejection detection, the serial queue (ordering, concurrency bound, failure isolation) and authorization-error classification
tests/plugin-apply.test.js Activation in a real cordis context, the command surface and disposal
tests/plugin-real-services.test.js Activation against the shipped ToolRuntime, CommandRuntime and SystemPrompt: command registration, prompt-section assembly, tool registration and the full execution pipeline (argument validation, output contract, error path)
tests/flowus-live.test.js Metadata discovery against the live endpoint, the PKCE authorization URL and the error mapping for an unauthorized connection

Distribution from a modified checkout:

npm pack --pack-destination "$env:USERPROFILE\.dsh\profiles\web"
dsh plugin --profile web add "file:./blksword-dsh-flowus-0.1.1.tgz"

Formal publication is described under Releasing.

Verifying against another runtime

The suite resolves @deepseek-ai/* through the node_modules\@deepseek-ai junction. To verify a different runtime, point that junction at the target release's package directory and re-run node --test and the live smoke tests from a copy of the repository. Two differences between releases are relevant:

  • Dependency layout. 0.2.0-rc.2 nests its dependencies under <dsh>\node_modules\@deepseek-ai\..., whereas 0.2.1-alpha.1 and 0.2.1-alpha.2 hoist them to <runtime>\node_modules\@deepseek-ai\..., with no nested directory. Tooling that assumes the rc.2 layout requires a fallback.
  • ./invariant subpaths were removed. @deepseek-ai/dsh-tools/invariant, dsh-commands/invariant and dsh-system-prompt/invariant (and their lib/invariant.js files) exist in 0.2.0-rc.2 and were removed in 0.2.1-alpha.1. This plugin imports package roots only; such deep imports must not be added.

Releasing

npm login                      # once per machine; the @blksword scope must belong to the account
npm publish --access public    # prepublishOnly runs the offline suite first
npm view @blksword/dsh-flowus version

# End-to-end check of the published artifact
dsh plugin --profile web remove @blksword/dsh-flowus
dsh plugin --profile web add @blksword/dsh-flowus@latest

dsh plugin add is the only supported installation path: the loader resolves the package from the profile's node_modules, and the client half is served from the package's exports["./client"].

Troubleshooting

Symptom Resolution
The /flowus command is unknown The plugin is not loaded. Verify that dsh.profile.bundles contains @blksword/dsh-flowus, then restart dsh web
No FlowUs page in the settings panel Same cause: the client entry graph is composed at process start. Restart dsh web and reload the browser page
A settings-page action returns 401 Page requests require the login cookie. Open the GUI from the ?token= URL printed by dsh web
No tools after authorization Run /flowus reload, then inspect the error line in /flowus status or on the settings page
Port 42813 is in use The plugin falls back to an ephemeral port, which requires a new client registration and may reach the registration rate limit. Configure a different fixed callbackPort
Registration throttled (too_many_requests) Retry the authorization later. The plugin reuses an existing registration, so registration is normally required only once
The browser did not open The authorization URL is printed by the command and displayed on the settings page and can be opened manually
Tool-name conflict Change serverName (for example to flowus2); the tool namespace changes accordingly

Known limitations

  • Image and audio content blocks are not rendered inline; they are presented as placeholder text. FlowUs tools are predominantly text- and JSON-oriented.
  • The MCP task extension is not supported: tools declaring execution.taskSupport === "required" are skipped and the reason is logged.
  • MCP resources and prompts are not registered; the scope of this plugin is tools, the command and the settings page.
  • The settings page provides state reporting and authorization/connection operations only. There is no per-tool enable switch; tool visibility is currently bounded only by maxTools and the naming-conflict guard.
  • The first authorization must be confirmed by a user in a browser, which is inherent to the OAuth authorization-code flow.

Repository layout

dsh-flowus/
├─ cordis.patch.yml      # bundle layer: inserts the flowus plugin row and defaults
├─ package.json          # dsh.bundle.patch, dsh.client, peer dependencies, publish whitelist
├─ icon.svg
├─ locale/               # display name and summary for the plugin list (en/zh)
│  ├─ en.json
│  └─ zh.json
├─ lib/
│  ├─ index.js           # plugin entry: Config, apply, tool publishing, /flowus, prompt section, page routes
│  ├─ http-api.js        # settings-page host JSON API (/flowus/api/*, behind the connection trust fence)
│  ├─ client.js          # browser half: __ModuleLoader__ bundle registering the settings page
│  ├─ login.js           # two-leg authorization-code orchestration
│  ├─ oauth-provider.js  # the SDK's OAuthClientProvider implementation, token summary and expiry checks
│  ├─ callback-server.js # one-shot loopback redirect receiver
│  ├─ browser.js         # shell-free browser launch with Windows candidate fallback
│  ├─ mcp-session.js     # MCP session: connect, discover, call and normalize results
│  ├─ tool-bridge.js     # MCP JSON Schema to DSH defineTool conversions
│  ├─ tool-names.js      # mcp__<server>__<raw> naming contract
│  ├─ command-grammar.js # /flowus grammar
│  └─ store.js           # private state file (atomic, serialized)
└─ tests/                # offline tests and optional live smoke tests

License

Apache-2.0. See LICENSE.