Workflow & Automation
dsh-flowus
BlkSword/dsh-flowus · ★ 0
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.
Manifest details
安装
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
Table of contents
- Scope
- Background
- Requirements
- Installation
- From npm
- From GitHub
- From a local checkout
- After installation
- Compatibility
- First authorization
- Commands
- Settings page
- Configuration
- Credentials and security
- Implementation
- Development and testing
- Verifying against another runtime
- Releasing
- Troubleshooting
- Known limitations
- Repository layout
- 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>. /flowuscommand. 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:
- installs the package through pnpm;
- reads the
dsh.bundle.patchdeclaration in the package and appends@blksword/dsh-flowustodsh.profile.bundles, so no manual edit ofpackage.jsonis required; - 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:/...andfile: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 relativefile: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-primitivesas a runtime dependency package (it is absent fromnode_modules\@deepseek-ai), while the settings-page classic scriptrequires 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-writejoined 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
- The plugin opens a one-shot callback listener on
127.0.0.1:42813/callback. - 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.
- After the user approves the request, the browser is redirected to the callback address.
- 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.
- 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
StateDotandTag. - 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 defaultC:\Users\<user>\.dsh\flowus\oauth.json) and contains the dynamically registeredclient_id, the token set, the PKCE verifier and the discovery cache. Writes prefer the harness primitives (@deepseek-ai/dsh-atomic-write: random-suffix sibling,wxexclusive 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 to0600where the platform supports it. Logout also prunes leftover<file>.*.tmpand<file>.corruptsiblings. - 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. logoutremoves the tokens and the verifier while retaining the dynamic client registration. The registration endpoint is rate-limited (an observedX-RateLimit-Limitof 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) andapi.flowus.cn(authorization, token and registration). - To revoke access, revoke the application authorization on the FlowUs side and then run
/flowus logoutlocally.
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_changedtriggers 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 reloadis required. - Transient-failure backoff. The nginx in front of
mcp.flowus.cnanswers 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 carrieslastErrorKind, so the page and/flowus statuscan say the failure is temporary and being retried. - Result bound and call cards. One result is truncated to
maxResultCharsbefore 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 whosekindseparates 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
GETonly and requires exactly onestatethat 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
serverUrlpoints 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/*throughctx.webServer.register; the client half is awindow.__ModuleLoader__classic script (lib/client.js, with no build step) registered into thesettings.sectionslot. The two halves communicate throughfetchand 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.mjslinks every@deepseek-ai/*package individually (a junction on Windows) instead of linking the whole scope directory. The difference is measurable:npm installdeletes 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-runnpm run link-runtimeafter 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.2nests its dependencies under<dsh>\node_modules\@deepseek-ai\..., whereas0.2.1-alpha.1and0.2.1-alpha.2hoist them to<runtime>\node_modules\@deepseek-ai\..., with no nested directory. Tooling that assumes the rc.2 layout requires a fallback. ./invariantsubpaths were removed.@deepseek-ai/dsh-tools/invariant,dsh-commands/invariantanddsh-system-prompt/invariant(and theirlib/invariant.jsfiles) exist in0.2.0-rc.2and were removed in0.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
maxToolsand 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.