staff-mcp 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +178 -49
  3. package/README_zh.md +176 -47
  4. package/dist/src/data/HttpDataSession.d.ts +22 -0
  5. package/dist/src/data/HttpDataSession.js +925 -0
  6. package/dist/src/data/HttpDataSession.js.map +1 -0
  7. package/dist/src/data/node.d.ts +4 -0
  8. package/dist/src/data/node.js +93 -0
  9. package/dist/src/data/node.js.map +1 -0
  10. package/dist/src/data/types.d.ts +54 -0
  11. package/dist/src/data/types.js +2 -0
  12. package/dist/src/data/types.js.map +1 -0
  13. package/dist/src/data-stdio-docker.d.ts +10 -0
  14. package/dist/src/data-stdio-docker.js +140 -0
  15. package/dist/src/data-stdio-docker.js.map +1 -0
  16. package/dist/src/docker-proxy.d.ts +6 -1
  17. package/dist/src/docker-proxy.js +324 -147
  18. package/dist/src/docker-proxy.js.map +1 -1
  19. package/dist/src/extensions/apps.d.ts +12 -0
  20. package/dist/src/extensions/apps.js +85 -0
  21. package/dist/src/extensions/apps.js.map +1 -0
  22. package/dist/src/extensions/config.d.ts +27 -0
  23. package/dist/src/extensions/config.js +91 -0
  24. package/dist/src/extensions/config.js.map +1 -0
  25. package/dist/src/extensions/protocol.d.ts +35 -0
  26. package/dist/src/extensions/protocol.js +47 -0
  27. package/dist/src/extensions/protocol.js.map +1 -0
  28. package/dist/src/extensions/runtime.d.ts +16 -0
  29. package/dist/src/extensions/runtime.js +266 -0
  30. package/dist/src/extensions/runtime.js.map +1 -0
  31. package/dist/src/file-manager/extension.d.ts +21 -0
  32. package/dist/src/file-manager/extension.js +514 -0
  33. package/dist/src/file-manager/extension.js.map +1 -0
  34. package/dist/src/file-manager/file-core.d.ts +128 -0
  35. package/dist/src/file-manager/file-core.js +750 -0
  36. package/dist/src/file-manager/file-core.js.map +1 -0
  37. package/dist/src/file-manager/transfers.d.ts +423 -0
  38. package/dist/src/file-manager/transfers.js +540 -0
  39. package/dist/src/file-manager/transfers.js.map +1 -0
  40. package/dist/src/file-manager/ui/index.html +81 -0
  41. package/dist/src/index.js +289 -128
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/lsp/manager.d.ts +2 -0
  44. package/dist/src/lsp/manager.js +22 -10
  45. package/dist/src/lsp/manager.js.map +1 -1
  46. package/dist/src/reverse/SseTransport.d.ts +34 -0
  47. package/dist/src/reverse/SseTransport.js +215 -0
  48. package/dist/src/reverse/SseTransport.js.map +1 -0
  49. package/dist/src/reverse/http.d.ts +7 -0
  50. package/dist/src/reverse/http.js +85 -0
  51. package/dist/src/reverse/http.js.map +1 -0
  52. package/dist/src/server.d.ts +4 -2
  53. package/dist/src/server.js +80 -18
  54. package/dist/src/server.js.map +1 -1
  55. package/dist/src/terminal/extension.d.ts +4 -0
  56. package/dist/src/terminal/extension.js +108 -0
  57. package/dist/src/terminal/extension.js.map +1 -0
  58. package/dist/src/terminal/pty.d.ts +14 -0
  59. package/dist/src/terminal/pty.js +121 -0
  60. package/dist/src/terminal/pty.js.map +1 -0
  61. package/dist/src/terminal/session.d.ts +136 -0
  62. package/dist/src/terminal/session.js +422 -0
  63. package/dist/src/terminal/session.js.map +1 -0
  64. package/dist/src/terminal/ui/index.html +183 -0
  65. package/dist/src/tools/file-tools.d.ts +2 -1
  66. package/dist/src/tools/file-tools.js +19 -7
  67. package/dist/src/tools/file-tools.js.map +1 -1
  68. package/dist/src/tools/mcp-client-tools.js +244 -104
  69. package/dist/src/tools/mcp-client-tools.js.map +1 -1
  70. package/dist/src/tools/shell-tools.js +1 -1
  71. package/dist/src/tools/shell-tools.js.map +1 -1
  72. package/dist/src/tools/skills.js +26 -1
  73. package/dist/src/tools/skills.js.map +1 -1
  74. package/dist/src/tools/system-tools.d.ts +9 -3
  75. package/dist/src/tools/system-tools.js +31 -48
  76. package/dist/src/tools/system-tools.js.map +1 -1
  77. package/dist/src/transports/http.d.ts +16 -2
  78. package/dist/src/transports/http.js +261 -59
  79. package/dist/src/transports/http.js.map +1 -1
  80. package/dist/src/transports/reverse.d.ts +23 -19
  81. package/dist/src/transports/reverse.js +130 -74
  82. package/dist/src/transports/reverse.js.map +1 -1
  83. package/dist/src/transports/stdio.d.ts +6 -2
  84. package/dist/src/transports/stdio.js +131 -19
  85. package/dist/src/transports/stdio.js.map +1 -1
  86. package/dist/src/utils/proxy.d.ts +3 -3
  87. package/dist/src/utils/proxy.js +84 -62
  88. package/dist/src/utils/proxy.js.map +1 -1
  89. package/dist/src/utils/server-activity.d.ts +6 -0
  90. package/dist/src/utils/server-activity.js +46 -0
  91. package/dist/src/utils/server-activity.js.map +1 -0
  92. package/dist/src/utils/server-lifecycle.d.ts +5 -0
  93. package/dist/src/utils/server-lifecycle.js +51 -0
  94. package/dist/src/utils/server-lifecycle.js.map +1 -0
  95. package/docs/file-manager.md +179 -0
  96. package/docs/terminal.md +66 -0
  97. package/package.json +40 -19
  98. package/dist/src/transports/fte-lifecycle.d.ts +0 -13
  99. package/dist/src/transports/fte-lifecycle.js +0 -76
  100. package/dist/src/transports/fte-lifecycle.js.map +0 -1
  101. package/dist/tests/docker-proxy.test.d.ts +0 -1
  102. package/dist/tests/docker-proxy.test.js +0 -395
  103. package/dist/tests/docker-proxy.test.js.map +0 -1
  104. package/dist/tests/file-write-queue.test.d.ts +0 -1
  105. package/dist/tests/file-write-queue.test.js +0 -105
  106. package/dist/tests/file-write-queue.test.js.map +0 -1
  107. package/dist/tests/fte-lifecycle.test.d.ts +0 -1
  108. package/dist/tests/fte-lifecycle.test.js +0 -118
  109. package/dist/tests/fte-lifecycle.test.js.map +0 -1
  110. package/dist/tests/lsp-client.test.d.ts +0 -1
  111. package/dist/tests/lsp-client.test.js +0 -52
  112. package/dist/tests/lsp-client.test.js.map +0 -1
  113. package/dist/tests/package-info.test.d.ts +0 -1
  114. package/dist/tests/package-info.test.js +0 -54
  115. package/dist/tests/package-info.test.js.map +0 -1
  116. package/dist/tests/proxy-no-proxy.test.d.ts +0 -1
  117. package/dist/tests/proxy-no-proxy.test.js +0 -156
  118. package/dist/tests/proxy-no-proxy.test.js.map +0 -1
  119. package/dist/tests/proxy.test.d.ts +0 -1
  120. package/dist/tests/proxy.test.js +0 -89
  121. package/dist/tests/proxy.test.js.map +0 -1
  122. package/dist/tests/reverse-reconnect.test.d.ts +0 -1
  123. package/dist/tests/reverse-reconnect.test.js +0 -89
  124. package/dist/tests/reverse-reconnect.test.js.map +0 -1
  125. package/dist/tests/security.test.d.ts +0 -1
  126. package/dist/tests/security.test.js +0 -21
  127. package/dist/tests/security.test.js.map +0 -1
  128. package/dist/tests/shell-tools.test.d.ts +0 -1
  129. package/dist/tests/shell-tools.test.js +0 -113
  130. package/dist/tests/shell-tools.test.js.map +0 -1
  131. package/dist/tests/test_files/test.d.ts +0 -1
  132. package/dist/tests/test_files/test.js +0 -4
  133. package/dist/tests/test_files/test.js.map +0 -1
@@ -0,0 +1,46 @@
1
+ const activities = new WeakMap();
2
+ function activityFor(server) {
3
+ let activity = activities.get(server);
4
+ if (!activity) {
5
+ activity = { requests: 0, installed: false, predicates: new Set() };
6
+ activities.set(server, activity);
7
+ }
8
+ return activity;
9
+ }
10
+ /** Install before business handlers. SDK-owned initialize/ping handlers stay untouched. */
11
+ export function installRequestActivity(server) {
12
+ const activity = activityFor(server);
13
+ if (activity.installed)
14
+ return;
15
+ const setRequestHandler = server.server.setRequestHandler.bind(server.server);
16
+ server.server.setRequestHandler = (schema, handler) => {
17
+ setRequestHandler(schema, async (request, extra) => {
18
+ activity.requests++;
19
+ try {
20
+ // Cancellation/transport closure does not settle the actual handler.
21
+ return await handler(request, extra);
22
+ }
23
+ finally {
24
+ activity.requests--;
25
+ }
26
+ });
27
+ };
28
+ activity.installed = true;
29
+ }
30
+ /** Predicates read their owner's existing state; they must not renew or sweep resources. */
31
+ export function registerServerActivity(server, predicate) {
32
+ activityFor(server).predicates.add(predicate);
33
+ }
34
+ export function hasServerActivity(server) {
35
+ const activity = activities.get(server);
36
+ if (!activity)
37
+ return false;
38
+ if (activity.requests > 0)
39
+ return true;
40
+ for (const predicate of activity.predicates) {
41
+ if (predicate())
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ //# sourceMappingURL=server-activity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-activity.js","sourceRoot":"","sources":["../../../src/utils/server-activity.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,GAAG,IAAI,OAAO,EAAuB,CAAC;AAEtD,SAAS,WAAW,CAAC,MAAiB;IAClC,IAAI,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACZ,QAAQ,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;QACpE,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,sBAAsB,CAAC,MAAiB;IACpD,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,QAAQ,CAAC,SAAS;QAAE,OAAO;IAC/B,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,CAC1D,MAAM,CAAC,MAAM,CAChB,CAAC;IACF,MAAM,CAAC,MAAM,CAAC,iBAAiB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;QAClD,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YAC/C,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACpB,IAAI,CAAC;gBACD,qEAAqE;gBACrE,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;oBAAS,CAAC;gBACP,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACxB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IACF,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC;AAC9B,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,sBAAsB,CAClC,MAAiB,EACjB,SAAwB;IAExB,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IAC/C,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,QAAQ,CAAC,QAAQ,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,KAAK,MAAM,SAAS,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC1C,IAAI,SAAS,EAAE;YAAE,OAAO,IAAI,CAAC;IACjC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ type Cleanup = () => void | Promise<void>;
3
+ /** One server owns one lifetime. Cleanup must not await server.close() itself. */
4
+ export declare function registerServerCleanup(server: McpServer, cleanup: Cleanup): void;
5
+ export {};
@@ -0,0 +1,51 @@
1
+ const lifecycles = new WeakMap();
2
+ function checkResults(results) {
3
+ const errors = results
4
+ .filter((result) => result.status === 'rejected')
5
+ .map((result) => result.reason);
6
+ if (errors.length)
7
+ throw new AggregateError(errors, 'Server cleanup failed');
8
+ }
9
+ /** One server owns one lifetime. Cleanup must not await server.close() itself. */
10
+ export function registerServerCleanup(server, cleanup) {
11
+ let lifecycle = lifecycles.get(server);
12
+ if (!lifecycle) {
13
+ const state = { cleanups: [] };
14
+ lifecycle = state;
15
+ lifecycles.set(server, state);
16
+ const startCleanup = () => {
17
+ if (!state.cleanup) {
18
+ // Invoke every callback immediately, including synchronous fences,
19
+ // without allowing one failure to skip the remaining owners.
20
+ state.cleanup = Promise.allSettled(state.cleanups.map(async (run) => {
21
+ await run();
22
+ })).then(checkResults);
23
+ // Transport onclose cannot await; explicit close observes this same result.
24
+ void state.cleanup.catch(() => { });
25
+ }
26
+ return state.cleanup;
27
+ };
28
+ const previousClose = server.server.onclose;
29
+ server.server.onclose = () => {
30
+ startCleanup();
31
+ previousClose?.call(server.server);
32
+ };
33
+ const close = server.close.bind(server);
34
+ server.close = () => {
35
+ if (!state.closing) {
36
+ // Defer SDK close until the shared promise is installed. onclose
37
+ // only starts cleanup and never waits for this close operation.
38
+ const transportClose = Promise.resolve().then(close);
39
+ state.closing = Promise.allSettled([
40
+ startCleanup(),
41
+ transportClose,
42
+ ]).then(checkResults);
43
+ }
44
+ return state.closing;
45
+ };
46
+ }
47
+ if (lifecycle.cleanup)
48
+ throw new Error('Cannot register cleanup on a closed server');
49
+ lifecycle.cleanups.push(cleanup);
50
+ }
51
+ //# sourceMappingURL=server-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-lifecycle.js","sourceRoot":"","sources":["../../../src/utils/server-lifecycle.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,GAAG,IAAI,OAAO,EAAwB,CAAC;AAEvD,SAAS,YAAY,CAAC,OAAwC;IAC1D,MAAM,MAAM,GAAG,OAAO;SACjB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC;SAChD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,MAAM;QACb,MAAM,IAAI,cAAc,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;AAClE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,qBAAqB,CACjC,MAAiB,EACjB,OAAgB;IAEhB,IAAI,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,MAAM,KAAK,GAAc,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAC1C,SAAS,GAAG,KAAK,CAAC;QAClB,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC9B,MAAM,YAAY,GAAG,GAAG,EAAE;YACtB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACjB,mEAAmE;gBACnE,6DAA6D;gBAC7D,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,UAAU,CAC9B,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;oBAC7B,MAAM,GAAG,EAAE,CAAC;gBAChB,CAAC,CAAC,CACL,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACrB,4EAA4E;gBAC5E,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,KAAK,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC;QACF,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE;YACzB,YAAY,EAAE,CAAC;YACf,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACvC,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,GAAG,GAAG,EAAE;YAChB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACjB,iEAAiE;gBACjE,gEAAgE;gBAChE,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrD,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;oBAC/B,YAAY,EAAE;oBACd,cAAc;iBACjB,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC1B,CAAC;YACD,OAAO,KAAK,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC;IACN,CAAC;IACD,IAAI,SAAS,CAAC,OAAO;QACjB,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAClE,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,179 @@
1
+ # Independent file manager extension
2
+
3
+ This document describes the staff-mcp 2.0.0 file extension. Reverse and extensions require chat-ai commit `66461d16090456d6f1fee21ce19088dceefd6658` or a later compatible version, with matching host/descriptors/UI assets. The authoritative protocol is `chat-ai/docs/protocol/README.md` at that compatible host version; both endpoints implement it themselves, with no required extension SDK. Standard stdio/Streamable HTTP tools/resources/prompts remain supported. No Chat8/schema9/format7/share protocol2 data migration is required. See [compatibility and upgrade](../README.md#200-compatibility-and-upgrade).
4
+
5
+ Extensions are off by default. Unsupported versions, unavailable optional features or failed data setup leave an otherwise healthy standard MCP connection usable, without enabling the affected App. This does not make the old reverse handshake compatible: reverse requires a paired upgrade even when extensions are disabled, with no old-protocol fallback or automatic transfer resumption.
6
+
7
+ Requires Node.js 22.22.2 or newer on the host and in Docker. Linux and Linux-host local Docker Engine are the primary full-extension environments. Windows native standard MCP (stdio and HTTP) and Windows Docker standard MCP have been validated, not full extensions. Non-Linux Docker launchers disable optional extensions; macOS is untested.
8
+
9
+ ## Tools, file apps, and Resources
10
+
11
+ - **Tools** retain their existing MCP names and `tools/call` behavior. A host must not infer a file app from tool metadata or wrap file-manager requests as tool calls.
12
+ - **File apps** are advertised by an experimental initialize capability. The host reads the declared HTML Resource and supplies a restricted bridge for direct user operations; no model invocation is required.
13
+ - **Resources** use standard MCP list, template, read, subscribe, and unsubscribe methods. Non-UI file Resources work without app negotiation: when files is enabled they use the configured file-app roots; otherwise they retain the standard workspace/allowed-directory scope. UI Resources require the corresponding enabled and negotiated app capability.
14
+
15
+ The initialize capability uses one discovery root:
16
+
17
+ ```json
18
+ {
19
+ "experimental": {
20
+ "cc.qqxing/extensions": {
21
+ "version": 1,
22
+ "extensions": {
23
+ "cc.qqxing/apps": { "versions": [1] },
24
+ "cc.qqxing/files": { "versions": [1] },
25
+ "cc.qqxing/data": { "versions": [1] }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ All extensions are disabled by default. The manifest above applies when files is explicitly enabled; a usable, explicitly enabled Linux PTY adds `cc.qqxing/streams: {versions:[1]}` (not a terminal manifest entry). Apps/data dependencies are derived automatically from enabled, available features; terminal alone does not enable files. Disabled features register no corresponding business handlers or UI Resources. With all extensions off there is no extension manifest, app/data handler or application data listener/session; ordinary tools and non-UI Resources remain. If the requested terminal's platform/native PTY/shell requirements are unavailable, staff warns and omits terminal/streams while retaining standard MCP and any available files capability. Negotiation is tied to the actual initialized SDK session, not a client name. Calls to `cc.qqxing/apps/v1/list {}` return `{apps:[...]}`. The `files` descriptor uses `ui://staff-file-manager/v1/index.html`, `mimeType:"text/html"`, `bridgeVersion:1`, `icon:"folder"`, `requiredExtensions:{"cc.qqxing/files":1,"cc.qqxing/data":1}`, and the ordinary `cc.qqxing/file-manager/*` methods listed below. It declares `capabilities.files:{version:1,upload:true,download:true}`; file lifecycle methods are owned by the files module, not descriptor-provided method mappings.
33
+
34
+ Old `experimental["cc.qqxing/apps"]`, `cc.qqxing/file-transfer/*` and private FTE chunks are not aliases or fallbacks; no backend MCP WS data path remains (the independent browser App stream WS is not a fallback). Ordinary tools and standard non-UI Resources do not require app negotiation; they remain available without granting implicit file-app or terminal access. A files manifest alone does not provide a binary path: negotiate data v1 and configure the current connection dataSession. Local stdio/Docker, direct HTTP and reverse use the same raw HTTP/consumption-ACK protocol; unsupported extensions do not affect standard MCP clients.
35
+
36
+ ## App and bridge boundary
37
+
38
+ The app is served by standard `resources/read` at the declared UI URI. It is not a hosted web URL and is not included in ordinary file resource listings. The current chat-ai host reads that HTML through its SDK, runs it in an opaque-origin `sandbox="allow-scripts"` iframe, blocks network access with CSP, and uses a source-checked MessageChannel handshake. Theme and enabled/connected lifecycle messages come from the host. The theme snapshot includes semantic colors, font families, base font size, and control radius; the app accepts a property-typed allowlist on the authenticated port. Toolbars, tabs, editor surfaces, and progress indicators use those tokens. This does not enable network access or load the host’s external web fonts; font families use locally available fallbacks.
39
+
40
+ The direct file-manager namespace is `cc.qqxing/file-manager/`. Its ordinary methods are `roots`, `list`, `stat`, `readText`, `saveText`, `createFile`, `mkdir`, `move`, and `delete`. Transfer preparation, finalization, and release are separate methods controlled by the host. The bridge is a generic app host, not an arbitrary tool executor, fetch proxy, shell, or filesystem API. The app translates file actions into its declared full method names; chat-ai does not interpret root/path/editing business parameters. The `mcp-app-ready` / `mcp-app-connect` handshake binds a private MessagePort. Separate app IDs on one MCP are independent launchable workspaces; transport extensions are not UI applications.
41
+
42
+ ## Lazy directory tree
43
+
44
+ The file navigator uses an on-demand tree for the currently selected root. Opening the app loads only the root’s first page (100 entries); expanding a folder lists only its direct children. Each folder owns its cached pages, loading/error state, cursor, and “Load more” action. After merging pages, the UI sorts that directory’s complete set of loaded entries with directories first and names ascending within the directory and non-directory groups; symlinks remain non-directories. Rendering and keyboard navigation use that same order. The server still paginates by name: until all pages are loaded, the visible entries are not guaranteed to include every directory, and loading another page may insert directories ahead of already-visible files. Sorting does not trigger extra metadata/text reads or fetch unrequested pages. Collapsing a branch hides its descendants without closing file tabs or discarding editor drafts, and reopening an already-loaded branch reuses its cache. No background traversal, eager document reading, filesystem watcher, or full-project index is started.
45
+
46
+ The tree cache is scoped to the current root and connection availability. Pending reads are fenced when that scope changes or a directory is invalidated, so late replies cannot repopulate an old branch. A failed folder can be retried independently. If the server rejects an expired cursor, that folder must be refreshed rather than appending an inconsistent page. Caches and expansion are not persisted across browser reloads.
47
+
48
+ The toolbar breadcrumb identifies the current directory, which is the destination for blank-area creation and upload actions; a directory context menu instead targets that directory. There is no separate target-description row. Refresh is an icon in the same toolbar. File names appear in the tree and editor tabs; their full path and listing metadata are available in tooltips instead of repeated headings, an editor-path label or a persistent selection footer. Selecting a folder chooses that folder; selecting a file or activating its editor chooses its parent. Refresh reads that directory’s first page again, resetting only its pagination rather than reloading the entire tree. Save/create/upload refresh their affected parent; rename/move refresh the source and destination parents already loaded by the tree and discard obsolete subtree caches; deletion clears the removed branch and moves an affected destination back to its parent. Open tabs still use their existing version checks, path updates, and draft confirmations. Upload completion refreshes the original upload destination even if navigation changed meanwhile.
49
+
50
+ On wide views (at least 900 CSS pixels inside the app), the tree and file content appear side by side. Narrow views and phones switch between the tree and content, keeping the same open tabs. Arrow keys move through visible nodes, right/left expand or collapse, and Enter/Space open or select; the existing context-menu key and Shift+F10 continue to work. Activating a tab can expand its ancestors, but never automatically consumes every pagination cursor to locate an item.
51
+
52
+ The UI renders only expanded branches. Its cost still grows with explicitly loaded, visible entries; this is not a virtualized million-file explorer. The existing server listing implementation reads and sorts a directory’s names before slicing a page, so pagination bounds response size, not the total work of enumerating an exceptionally large single directory. The tree does not change that backend contract or claim to improve network bandwidth. Markdown preview, drag-and-drop moves, Git decorations and recursive search are outside this UI change.
53
+
54
+ ## File actions and adding to a conversation
55
+
56
+ Tree rows and blank space open contextual menus through desktop right-click, touch/pen long press (about 500 ms), or the keyboard context-menu key / Shift+F10. A hold is cancelled by movement beyond 10 CSS pixels, scrolling, pointer cancellation, navigation, lifecycle changes, or unmounting. It does not prevent ordinary taps or scrolling. After a successful hold only its following synthesized click is consumed; a new physical gesture and keyboard activation are unaffected. The menu supports keyboard navigation and Escape, stays within the iframe viewport, and closes on navigation, scrolling outside the menu, resize, or lifecycle changes. Actions bind to the row that opened the menu, not a previous selection. There are no row “⋯” buttons, tree-local current-directory “⋯”, global file-operation menu, or persistent bottom selection summary. Hover details use the listing metadata; selecting a row does not issue an extra metadata-only stat request. Mutation actions still fetch the latest version before opening their confirmation dialog.
57
+
58
+ The only menu entry points for creating a file/folder and choosing upload files are directory and blank-area tree context menus. A directory row targets that directory; blank space targets the current breadcrumb directory, including at an empty root. File menus retain their file operations. Transfer history is app-wide, not scoped to a tree location: a dedicated transfer icon beside the toolbar refresh button opens or collapses the existing drawer, including when the directory is empty, read-only, or disconnected. It is not offered in tree context menus. The drawer retains records across directory navigation, its progress/attention indicators, and its existing clear/close actions. The toolbar retains navigation, refresh, and this global transfer control, not create/upload controls. Upload selection captures its root and directory before opening the device picker: changing the selection or current directory while the picker is open cannot redirect the files. A changed root/connection lifetime rejects the old selection instead of reusing stale scope. Read-only/unavailable destinations remain blocked.
59
+
60
+ The focusable tree also accepts paste events containing actual browser `File` objects. Selecting a directory targets it, selecting a file targets its parent, and an unselected tree targets the current directory. Plain text and pastes into the editor, inline name fields, or other editable elements are not intercepted. FileList and clipboard items are alternate representations, not two upload batches: the populated FileList is preferred, with items as fallback, and duplicate object references are removed without merging distinct same-name files. Default screenshot names such as `image.png` and empty names receive timestamp/random names with an extension; ordinary named files retain their names. No asynchronous clipboard permission API is used, and a paste without file objects never creates an upload or reports success.
61
+
62
+ External file drops upload only within the tree. A directory row targets that directory, a file row targets its parent, and blank space targets the current directory; a lightweight row/background highlight marks the physical drop location. The target is resolved directly from the drop event, never by changing selection and reading it later. Folder paste/drop payloads are rejected without recursively scanning their contents. Outside the tree, external file drops are prevented from navigating the browser and do not start uploads; ordinary editor text dragging remains available. Picker, paste, and drop all use the existing upload queue, overwrite/version confirmation, cancellation, and refresh of the original destination after completion, even after navigation. Actual uploads expand the existing transfer drawer. None of these inputs broadens the iframe sandbox or changes the bridge/transfer protocol.
63
+
64
+ New files and folders share an inline name field under the target tree directory, labeled “文件名” for files and “文件夹名” for folders. Only the name is requested: Enter or the explicit Create button confirms, Escape cancels, and IME composition must not submit early. These actions use button clicks and an explicit Enter handler, not form submission, because the iframe does not allow forms. The destination root/directory is captured when naming starts; navigation, root or lifecycle changes cancel the naming UI rather than redirecting a late action. Pending submissions are guarded against duplicate Enter/click requests, and late replies cannot change a newer naming operation.
65
+
66
+ Creating a file immediately calls the existing atomic `createFile` with empty text; folders use `mkdir`. Existing names are rejected, never overwritten. Success refreshes only the original parent directory and does not open an editor or create an unnamed tab. Users click the created file later to open, edit and save it with the existing version-checked `saveText` flow. All file tabs represent existing server files; unsaved edits keep their existing dirty/close/leave protections. File/folder creation does not add a new backend API or persist browser drafts across reloads.
67
+
68
+ Open, download, rename, move, copy path, and delete keep their existing business checks. Mutations still fetch a version for the intended target and use the same confirmation flow; deletion is not performed by opening the menu. Directories do not expose file-attachment/download actions, and symlink restrictions remain unchanged.
69
+
70
+ Regular files can be added to the host conversation from the row menu or editor. The app constructs the canonical `staff-file://` Resource URI by encoding each literal path component once, then invokes the generic bridge `attachResource { uri, title }`. The host reads the actual Resource through the bound MCP and returns `{ attached: true, name }`; the iframe does not supply the attachment body. The existing 256 KiB Resource limit applies, independently of the larger editor/transfer limits. This does not automatically send a message or invoke a model.
71
+
72
+ If any open tab for that file has unsaved changes, adding is blocked with an explicit save-first message. It never implicitly saves or silently attaches an older server copy. The add button shows its pending state; failures remain visible as errors. Successful adds do not create a persistent confirmation banner or a second attachment-presence state in the file app: the conversation input is the authority for its current attachments. Users return through the host workspace controls. Later external file edits do not update the already-added attachment snapshot.
73
+
74
+ ## Roots, paths, and concurrent edits
75
+
76
+ Files is enabled by `--extensions files` (or `--extensions files,terminal`). Its default root is `main=<working-dir>`. Repeatable `--file-root id=path` options replace the entire default root list; include `main` explicitly if it should remain. Root IDs must be unique lowercase letters/digits/hyphens, and relative directories resolve from the launching process cwd. The programmatic `ExtensionOptions.fileRoots` equivalent is `{rootId,directory}[]`; see [CLI/API examples](../README.md). These roots apply to the file app, files v1 and its standard file Resources, not standard tools' `--allowed-dir` permissions or terminal's default workspace cwd. With files disabled, file-root configuration does not alter standard Resources.
77
+
78
+ Explicit `--file-root system=/` selects the filesystem visible to staff, subject to actual OS access. In Docker, explicit host roots are bind-mounted at `/opt/staff-file-roots/<id>`: `system=/` deliberately exposes the host root there. It does not grant new OS privileges or imply support for every Docker/platform topology. File roots and protected-path checks are not a machine-wide security boundary against an arbitrary shell running as the same account.
79
+
80
+ Public paths are root-relative literal names, not host absolute paths or URL-encoded paths. Traversal, symlink traversal, protected roots, and private staging access are checked by the file core. Percent signs in RPC paths are literal filename characters; only the resource URI boundary decodes each component once. Root IDs are session/configuration context, not portable filesystem permissions.
81
+
82
+ Text reads and edits are capped at **2 MiB**. `saveText` requires the version returned by the read; overwriting an existing upload target likewise requires `expectedVersion`. Shared file-core operations serialize supported writes, recheck versions before publication, and use atomic publication rather than editing a target incrementally. `VERSION_CONFLICT` preserves the editor draft; the UI does not silently force an overwrite. These checks are not a promise to sandbox arbitrary external processes with access to the same filesystem.
83
+
84
+ `stat` returns metadata without reading/hashing file content by default. Call `stat` with `includeVersion: true` only when a mutation needs a content version; `readText` returns the version along with text. File selection/opening must not hash a large binary merely to display its name. Atomic replacements preserve the target permission bits, including executable bits. Upload copy and download snapshot/hash happen outside the shared write mutex; bounded final source/destination checks remain coordinated. External writers are detected through the documented version/identity checks, not locked out by this service.
85
+
86
+ Private staging is under the workspace's `.staff/file-manager/`; supported file tools and extension browsing exclude/protect that area. The Docker wrapper also derives its aliases from the bind mounts it creates, including overlapping file roots: aliases cannot be read, written, listed, moved or deleted through file APIs, and their ancestors cannot be moved or deleted. A root mounted from within staging is protected in its entirety. This is a path guard for wrapper-owned mounts, not a general bind-mount or OS isolation mechanism; arbitrary extra `-D` mounts and same-user shell access are outside that guarantee. Do not treat staging files or transfer handles as user attachments or ordinary Resources.
87
+
88
+ ## Standard file Resources and attachments
89
+
90
+ `resources/list` publishes root manifests and immediate regular files at each root, with pagination. It is not a recursive index of all descendants. `resources/templates/list` publishes `staff-file://<rootId>/{+path}` templates for nested paths. A URI encodes each path component once; the server checks canonical encoding and root-relative path validity.
91
+
92
+ `resources/read` returns real text, Base64 blob content, or a directory manifest. A directory manifest follows the file listing contract, including any next cursor; it is not a recursive archive. The server caps each resource at **256 KiB**, explicitly rejects excess data, and does not truncate. Subscriptions notify clients of changes made through the shared file core; this is not a general filesystem watcher for every external edit.
93
+
94
+ In chat-ai, adding a file is an explicit action inside the app; the input draft offers a preview of the attached snapshot, without a separate resource workspace. Attachments are snapshots of the returned contents, not URI-only placeholders or live links. Text uses the existing text ContentPart; supported image blobs use data URLs and image ContentParts; other blobs use existing file ContentParts. Source metadata stays in the input draft, while sending uses the normal chat content pipeline. The host also checks the aggregate decoded content limit of 256 KiB and excludes `ui:` and declared app HTML from ordinary attachments. Browsing does not invoke a model or automatically save a message.
95
+
96
+ ## Authorization and cross-chat lifecycle
97
+
98
+ staff-mcp enforces its configured file-path boundaries and per-session negotiation, not an OS sandbox around arbitrary same-account processes. It does not interpret chat-ai user roles or chat IDs. A host must enforce its own user identity and current grant before dispatching requests.
99
+
100
+ The current chat-ai integration uses the authenticated physical user namespace, reads persisted current-chat grants on each operation (including inherited role settings), and uses persisted user defaults for the home/new-chat scope. Runtime bindings include the exact SDK server instance, connection UUID/generation, and app descriptor. Reconnecting invalidates old references, even if the configured server name is unchanged.
101
+
102
+ The host keeps the iframe mounted across chat routes so in-memory tabs and unsaved drafts can survive a chat switch. An app is not permanently bound to its first chat; reuse still requires the new chat's current permission for the same connection. The current UI cancels active transfers when switching scopes or losing permission/connection rather than promising transparent cross-chat continuation. Apps without the current chat’s MCP grant are hidden from the launcher and workspace, while their iframe drafts remain mounted in the background. Returning to an authorized chat restores access to those drafts. Authorized but disconnected apps can still display drafts for copying, without writing through stale references. Refresh, browser exit, and process restart do not restore drafts or resume transfers.
103
+
104
+ Cleanup is deliberately narrower than file access: an owner can cancel/close their existing app's temporary transfers even after the grant is revoked, without gaining a new read, write, or commit permission. chat-ai expires idle app bindings after two hours and idle transfers after five minutes.
105
+
106
+ ## files v1 prepared transfers over a shared dataSession
107
+
108
+ The file app and chat-ai's existing `transfer_file` tool share `cc.qqxing/files/v1/*`, not separate app and FTE lifecycles. Ordinary file CRUD retains its existing namespace and schemas. Transfer parameters are strict objects:
109
+
110
+ | Method suffix | Parameters | Result / meaning |
111
+ | --- | --- | --- |
112
+ | `roots` | `{}` | configured logical roots |
113
+ | `resolve` | `{uri}` | `{rootId,path}` after validating a local `file://` URI against those roots |
114
+ | `prepare` (read) | `{operationId,mode:"read",rootId,path}` | stable snapshot metadata |
115
+ | `prepare` (write) | `{operationId,mode:"write",rootId,path,size,sha256?,overwrite?,expectedVersion?}` | private staging; overwrite defaults false, true requires expectedVersion, false forbids it |
116
+ | `commit` | `{operationId,transferId,sha256}` | committed result `{path,size,sha256,version}` or the operation's current state |
117
+ | `status` | `{operationId}` | bounded state/result lookup, never resumes a transfer |
118
+ | `renew` | `{operationId}` | `{operationId,expiresInMs:30000}` |
119
+ | `cancel` / `release` | `{operationId}` | shared cleanup; `TOO_LATE` after commit admission |
120
+
121
+ The host generates operationId before the first asynchronous prepare. Prepared metadata includes `operationId,transferId,state,loaded,name,size,expiresInMs,recordTtlMs`; reads also return `sha256,version`. The request field is **mode**, not direction; the lease field is **expiresInMs**, not leaseMs. Reads reject write-only fields. Same operation and parameters deduplicate within that live session; changed parameters or commit digest produce `IDEMPOTENCY_CONFLICT`. Cleanup before a delayed prepare leaves a bounded cancellation tombstone.
122
+
123
+ All transports return the same prepared resource metadata, not a file-specific HTTP ticket. The host opens `purpose:"cc.qqxing/files",resourceId:transferId,direction:"host-to-remote"|"remote-to-host"` through the current shared dataSession, also used by terminal. Only a live, unused prepared resource can be bound once. `cc.qqxing/data/v1/configure {binding,origin?}` establishes the connection; `cc.qqxing/data/v1/open {binding,open,link?}` negotiates a temporary `DataLink` with version, endpoint, channel, token, binding, open and expiresInMs. expiresInMs is a relative positive TTL bounded by the open timeout, measured using local monotonic time rather than peer wall-clock timestamps. No numeric WS channel ID or file-specific `/mcp/transfers` router remains.
124
+
125
+ With files enabled and data v1 negotiated/configured, direct HTTP uses `/mcp/data/<binding>` on the existing MCP listener, mounted before JSON parsing. For direct HTTP only, configure may include the host's configured HTTP(S) origin (no path/credentials/query/fragment) for HTTPS proxy advertisement; it does not authorize outbound fetch or trust Forwarded headers. Without it the initial request authority is used; stdio/reverse reject origin. Local stdio lazily starts a same-process 127.0.0.1 random-port listener at configure; stdout remains standard MCP JSON-RPC. The local Docker wrapper automatically publishes a private container port to a random 127.0.0.1 host port and passes baseUrl in read-only 0600 metadata, inspecting only its own cidfile container. This Docker application-data publication exists only when at least one extension is enabled; its listener remains lazy until configure. With all extensions off, dataAuto is false and no application-data publication, metadata mount, bootstrap or dataSession is created; ordinary workspace/package mounts remain. Non-Docker stdio has no application listener before configure. HTTP/reverse add no extra publication, and HTTP retains its standard MCP listener even with extensions off. The full-extension stdio Docker path requires Linux, reachable shared host loopback and a local daemon. Non-Linux Docker launchers and unsupported stdio data topologies warn and omit optional extensions, preserving standard tools when the base connection is usable; macOS is untested. Bootstrap/configure failure leaves standard MCP alive and data-dependent Apps unavailable. See the README for actual mount/daemon prerequisites; this does not add a remote tunnel or a legacy data fallback. Metadata/container uid and mount uid/gid permissions and native ABI/libc must match; no automatic chown or permission relaxation. See [README topology limits](../README.md).
126
+
127
+ Reverse control uses SSE + POST on the existing Next Web port. The host includes its data link in open; remote validates that exact configured Web origin/session path and actively GETs/PUTs, without an inbound listener. Direct HTTP/stdio return the staff endpoint link for host access instead. Both use raw binary `get` GET / `put` PUT, cumulative ACK (`ack` POST for GET consumption, `ack` GET stream for PUT consumption), and `close` DELETE. Bearer token and X-Data-Binding scope access; no browser Origin, arbitrary URL, redirects or replay. Tickets never reach the iframe. Proxy/CA-aware HTTP clients are injected by the connection; actual proxy streaming still needs validation.
128
+
129
+ Deliveries are at most 64 KiB, with a **256 KiB sliding consumption window**, allowing multiple blocks in flight rather than per-block stop-and-wait. `write` resolves after bounded copy/queue admission; a full window waits for cumulative receiver ACK. ACK follows awaited serial receiver.data/end, not socket/fetch completion, and end/closed success requires the final consumption ACK. ACK emission coalesces cumulative offsets into one in-flight send plus one latest pending value; normal receiver.data does not await ACK RTT, and EOF flushes the final ACK. Concurrent write calls are rejected rather than queued without bound. File EOF/credit is not commit. All transports use the same TransferManager and commit checks; no FTE/Base64 fallback. `transfer_file` retains its model name/parameters and file URI validation; source and destination each consume an owner transfer slot and relay bounded bytes, never a whole-file host buffer or a locally opened remote path.
130
+
131
+ Uploads progress through prepared/streaming/ready before an explicit digest-checked commit. Downloads read a private version-checked snapshot and then release it, never commit or delete the source. Writes publish atomically through the shared file core; network I/O does not hold the global write mutex. A cancellation admitted before commit prevents publication; after commit admission it is `TOO_LATE`. `status` can return preparing, prepared, streaming, ready, complete, committing, committed, released, expired, failed, or unknown as applicable. Explicit failures include `published:false`; unknown or an unavailable record is not proof of no publication. A lost commit receipt must not trigger an automatic write retry, re-prepare or protocol fallback.
132
+
133
+ Current limits are **100 MiB per file**, **two active transfers per session**, **128 operation records**, a **30-second remote monotonic lease**, and **five-minute final-state record retention**. The host renews after rechecking authorization, normally within ten seconds, and has its own five-minute idle-transfer timeout. Remote record retention is not an idle lease or persistent transaction journal; reconnect/restart invalidates old handles. Normal session close waits for pending preparation/byte I/O and any admitted commit to settle before removing private staging; it does not relabel an admitted or unknown publication as released or rolled back. The synchronous exit fallback remains registered until asynchronous staging cleanup succeeds. Process crashes can leave private staging; this is not a disk-quota or cross-restart recovery service. The common dataSession defaults to **four** active logical channels, a **15-second** open/missing-leg timeout and a **60-second** consumption-progress timeout, checked only while bytes await consumption ACK, end awaits final confirmation, a receiver callback is running, or an ACK is pending/in flight. A healthy idle PTY with no pending consumption/end work is not closed merely for having no bytes for 60 seconds; business leases and control-connection liveness still apply. These data-layer limits do not replace business leases or bound whole-process memory.
134
+
135
+ The current browser download still collects chunks into a Blob; server streaming is not zero browser buffering or a browser-to-staff direct path. Reverse bandwidth and latency depend on the deployment network and consumer speed. Package/native ABI/libc compatibility is a deployment requirement, not a property of the HTML bundle.
136
+
137
+ The UI expands transfer feedback on download/upload actions. Download preparation has indeterminate progress, receiving reports actual bytes, and a complete validated file waits for an explicit “Save to device” action instead of automatically opening a delayed browser dialog. The generic bridge uses `download` to prepare, `downloadReceive` to acknowledge and start bytes, `downloadSave` to hand the Blob to the browser, and `downloadDiscard` to release it. A cancel during preparation is confirmed only after the late handle is released. Browser handoff does not certify a local disk write. Across all apps, the current host allows two download slots (including receiving, ready files, and short-lived handoff URLs); ready Blobs expire after five minutes, handoff URLs after one minute. Clearing finished records does not discard a ready file.
138
+
139
+ Image preview is limited to 2 MiB. Cancellation admitted before the final commit prevents publication; once commit has started the host returns `TOO_LATE` rather than claiming an already-issued write was undone.
140
+
141
+ Terminal is a separate human app, not an implicit capability of enabling files; the host does not add a separate terminal permission switch. It requires the existing current-chat MCP grant, staff terminal extension enablement, Linux optional `node-pty`, and negotiated streams/data v1 with a usable duplex dataSession over supported local stdio/Docker, direct HTTP or reverse topology. Its starting cwd is not an OS sandbox, and it runs as the staff uid. Terminal I/O is not added to model context or chat history. It uses the generic browser-host `/api/mcp/app-stream` WebSocket (`chat-app-stream-v1`) with rendering-consumption ACKs, a separate 120-second browser-presence deadline and server-returned remote lease (staff defaults to 30 seconds), not the file app's two-hour TTL. Only the browser host refreshes presence every 10 seconds; the generic stream module schedules remote renewal from the returned lease; opening the app starts a PTY once, without another open button or high-risk confirmation. Exit/failure leaves the app shell available for manual reconnect, never automatic replay. Backpressured process exit can have incomplete output: current source reports remote `status.error:{code:"OUTPUT_INCOMPLETE",message:string}`, and host generic stream-frame `incomplete?:boolean` / `error?:{code:string,message:string}` on abnormal close/status, with UI warning handling. The generic `incomplete` flag belongs to the host stream result, not remote status; the app-local terminal adapter maps it to its presentation warning. These diagnostics detect possible loss; they neither recover missing bytes nor guarantee lossless output, even with exit code 0 or EOF. See the [terminal UI contract](terminal.md) and chat-ai's authoritative terminal protocol for lifecycle and limitations.
142
+
143
+ A configured optional file root that cannot initialize disables files with a stderr warning; it does not replace or widen the standard workspace tool roots. Invalid CLI syntax is still an error, and workspace/mount access errors still fail the relevant startup or file/tool operation rather than granting access. Availability and extension-version negotiation remain distinct from authorization.
144
+
145
+ ## Building and shipping the UI
146
+
147
+ From a development checkout with dependencies installed:
148
+
149
+ ```sh
150
+ npm run build
151
+ ```
152
+
153
+ The build compiles server TypeScript, type-checks the browser UI via `tsconfig.ui.json`, and runs `scripts/build-file-manager.mjs`. esbuild bundles React, CodeMirror, application code, and CSS into the self-contained `dist/src/file-manager/ui/index.html`. No external CDN or browser network request is required to render it.
154
+
155
+ The HTML is loaded relative to the compiled extension module. The package includes `dist/src`, so it carries this generated Resource, including when staff-mcp is read-only mounted into a Docker container. `prepublishOnly` runs the build; plain server compilation or `tsx src/index.ts` alone does not create the HTML Resource at the compiled module location. Rebuild and distribute the package for UI changes instead of relying on a developer's working-directory asset.
156
+
157
+ Deploy matching host bridge and staff-mcp assets, reconnect MCP and reopen Apps after upgrading. An already-open iframe is not hot-replaced by publishing a new package.
158
+
159
+
160
+ ## Host boundary and bridge v1
161
+
162
+ The descriptor advertises `bridgeVersion:1`, `scope:"workspace"` and `capabilities.files:{version:1,upload:true,download:true}`. File CRUD remains in `methods`; managed files lifecycle calls cannot bypass the files module through ordinary RPC. The iframe handshake and message envelopes retain version 1; the 2.0.0 extension contract requires matching host assets. The file UI now calls generic `readBytes` and receives only bounded `{data:base64}`; it selects raster-image MIME itself. The host does not interpret filename extensions. `maxBytes` is a client receive budget, removed before remote prepare; host inline bytes default to 2 MiB and host relay/download budget to 100 MiB. These budgets do not redefine the server's file/text/path limits. Deploy matching host/descriptors/UI assets and reopen existing Apps; previous byte-preview methods have no compatibility fallback.
163
+
164
+ The independent files-v1 contract adds `stat {rootId,path}->{version}` for opaque file-version lookup. It uses the same FileCore safety checks, does not prepare a snapshot or transfer, and rejects directories. Agent force-copy no longer calls the private file-manager/stat App method. Older peers without files/v1/stat fail that capability explicitly; clients must not omit expectedVersion, use a digest as a version, or replay the write. Existing non-force operations keep their request/response shape.
165
+
166
+ The HTTP data kernel accepts structurally valid purposes only through an explicit outer capability gate. The staff built-in runtime registers only enabled/negotiated files and terminal resources. The host admits negotiated files plus the purposes in current parsed streams-capable Apps, read lazily after discovery. Accepting new purpose syntax does not grant unknown business access.
167
+
168
+ ### HTTP idle policy
169
+
170
+ HTTP listener idle expiry reads existing resource activity: preparation, a valid transfer lease, queued byte I/O, cleanup and an admitted commit keep the session alive. Expired resources with no pending I/O and finished operation receipts do not. A control cancellation/HTTP disconnect is not proof that a write or commit has stopped. The policy never renews a file lease or changes published/unknown outcomes. A browser draft alone is not a server lease; reconnect and reopen the App before new operations after HTTP session expiry. See the README for listener capacity, defaults and configuration.
171
+
172
+
173
+ ### Capability composition and streams
174
+
175
+ File UI/CRUD and file publication remain business-owned; host FilesBridge/resource modules preserve operation IDs, quotas, leases, commit/status and unknown-publication handling. Generic AppFrame/MCPAppService/ChatMCPServer do not interpret file-manager or PTY parameters. New Apps that reuse supported capabilities do not require host-core changes. New host permissions/capabilities must be explicitly registered at discovery/validation, server resource-module and browser-bridge composition roots; they cannot be granted by a descriptor alone.
176
+
177
+ A leased byte App declares `capabilities.streams:{version:1,purpose,create,renew,status,close,commands?}` and requires streams/data v1 plus chat scope. Descriptor lifecycle/command values are distinct namespaced RPC methods, excluded from ordinary `methods`. Generic create uses `{operationId,params}` and returns `{resourceId,leaseMs,metadata?}`; renew uses `{operationId,resourceId}`, status returns active/closed plus opaque metadata/error, close uses `{operationId,reason:"user"}`, and commands use `{resourceId,params}`. Host exposes only its own streamId, not remote resource IDs or raw tickets. It preserves bytes, uses raw-byte cursors and browser-only Base64, and waits for real consumption ACK/EOF; abnormal close retains buffered bytes and marks `incomplete`, never clean success. Terminal business conversion belongs to staff's extension/UI adapter; see [terminal](terminal.md).
178
+
179
+ Reverse transport sources can later be extracted to MCP Client (host)/Server (remote) SDKs; no package split is done now. HTTP/TLS/proxy policy in `src/reverse/http.ts`, factory/extension/data setup and reconnect in `src/transports/reverse.ts`, and host Web auth/management remain integration. Transparent transports do not intercept data RPC; reverse wire and raw HTTP stay unchanged.
@@ -0,0 +1,66 @@
1
+ # Terminal app UI
2
+
3
+ This document describes the staff-mcp 2.0.0 terminal extension. Reverse and extensions require chat-ai commit `66461d16090456d6f1fee21ce19088dceefd6658` or a later compatible version, with matching host/descriptors/UI assets. The authoritative generic stream contract is `chat-ai/docs/protocol/streams.md` at that compatible host version; `chat-ai/docs/protocol/terminal.md` defines only the PTY business adapter, safety boundaries and UI differences. No Chat8/schema9/format7/share protocol2 data migration is required. See [compatibility and upgrade](../README.md#200-compatibility-and-upgrade).
4
+
5
+ Requires Node.js 22.22.2 or newer on the host and in Docker. Linux and Linux-host local Docker Engine are the primary full-extension environments. Windows native standard MCP (stdio and HTTP) and Windows Docker standard MCP have been validated, not full extensions. Non-Linux Docker launchers disable optional extensions; macOS is untested. Unavailable or incompatible optional capabilities must not break an otherwise healthy standard MCP connection. The old reverse handshake itself is incompatible even with extensions disabled: upgrade both endpoints rather than relying on extension degradation. There is no old-protocol fallback or automatic terminal input replay.
6
+
7
+ `terminal` is an independent, human-operated app enabled with `--extensions terminal` or `--extensions files,terminal`. It requires Linux, a loadable optional `node-pty`, an executable shell, and streams/data v1 with a usable duplex dataSession. All extensions are disabled by default; apps/data dependencies and terminal's streams v1 manifest are derived automatically. When Linux/native PTY/shell prerequisites are unavailable, staff warns on stderr and disables terminal/streams without failing standard MCP or an available files extension; the default disabled state does not load native PTY code. Non-Linux Docker launchers disable optional extensions before starting the container; standard MCP tools/resources/prompts remain available when the base connection is usable. Local stdio/Docker, direct HTTP and reverse share that public raw HTTP data protocol; standard MCP control transports remain unchanged. The host uses the existing current-chat MCP grant and same-origin browser requests, without a separate terminal permission switch or high-risk confirmation. HTTP/HTTPS follows the site-wide deployment policy, with no terminal-only TLS gate or origin allowlist. HTTPS is recommended for the whole site; HTTP does not encrypt login sessions, tool calls or terminal traffic. Topology and CLI details are in the [README](../README.md). The self-contained HTML Resource is built to `dist/src/terminal/ui/index.html`; terminal bytes do not add model tools, chat messages or chat history.
8
+
9
+ ## Opening and stopping
10
+
11
+ `--shell` takes an absolute executable file path. Without it, a valid absolute executable `$SHELL` is used; otherwise selection falls back to `/bin/bash`. The shell starts with `-i`, honors normal user startup files and preserves PATH, virtual environments and other staff environment variables. [`src/terminal/pty.ts`](../src/terminal/pty.ts) filters only these exact internal control keys: `STAFF_GLOBAL_DIR`, `STAFF_MCP_IS_DOCKER`, `STAFF_MCP_DATA_BOOTSTRAP`, `STAFF_MCP_DATA_HOST`, `STAFF_MCP_DATA_PORT`, `MCP_REVERSE_URL`, `MCP_REVERSE_TOKEN`, `MCP_REVERSE_NAME`, `MCP_HTTP_TOKEN`, `STAFF_MCP_HTTP_TOKEN`, `STAFF_MCP_REVERSE_TOKEN`. Missing `TERM` becomes `xterm-256color`, and `SHELL` is set to the selected executable. This is not a general secret scrubber or environment sandbox; the same OS identity may still access credentials and other files.
12
+
13
+ Selecting the Shell App automatically starts one PTY after the host bridge becomes usable and the visible xterm viewport has been measured with its fonts ready; there is no additional open button or high-risk confirmation. The initial PTY uses those fitted columns and rows rather than starting at 80×24 and resizing after the first prompt. Hidden, zero-sized or not-yet-measurable views wait for a usable layout. The toolbar reserves its control height before connecting, so showing the buttons does not invalidate the initial row measurement; subsequent viewport changes continue through the normal resize path. The toolbar shows the returned shell and starting path. The PTY defaults to staff's workspace `main`, independently of file-app roots, and inherits staff's environment and OS identity, not a separate browser-user identity; its cwd is not an operating-system sandbox. Shell startup configuration and history are not suppressed, so the remote shell or programs may record content even though chat-ai does not add terminal bytes to chat history.
14
+
15
+ **清屏** clears only the local terminal display and scrollback; it sends no shell command. Closing the terminal disables input immediately and requests remote cleanup. Closing cannot undo filesystem/network effects and does not guarantee every descendant process has ended. Normal server/session shutdown waits for the owned PTY exit, including the existing TERM-to-KILL escalation, before CLI exit; this does not change normal output/ACK draining or promise cleanup after SIGKILL. Input, including Ctrl-C and pasted text, goes to the PTY in order; uncertain acknowledgements are never replayed. Normal exit or PTY failure stops the current PTY but preserves the app shell for manual reconnect, not automatic restart.
16
+
17
+ Hiding the browser page or switching/collapsing app views within the same chat keeps the existing App and PTY. Visibility gates new opening, input and resize requests, not instance identity: already-admitted input, output reads and xterm consumption acknowledgements continue. Returning fits the viewport and enables input on that same PTY, without opening another shell. Genuine host lifecycle/permission loss, chat changes, explicit close, non-BFCache page exit and unmount still fence the instance, discard unsent input and request best-effort cleanup. An actually stopped or failed PTY requires manual reconnect; visibility restoration does not recreate it. Temporary settings loading or catalog-request errors are not confirmed revocation/disconnection. Real lease expiry, grant revocation or connection retirement still fences the PTY. Failed cleanup relies on host cleanup and the remote lease; a disconnected page cannot guarantee that its close request arrived. A pending open may return its terminal ID after the local instance was invalidated, so the host must fence and clean late creation results.
18
+
19
+ ## Host bridge contract
20
+
21
+ The app uses the shared Apps bridge. It announces `mcp-app-ready` and accepts the parent `mcp-app-connect` v1 handshake with nonce, instance ID, and one MessagePort. Theme snapshots and `{type:'lifecycle', enabled, connected, visible, message?}` use that authenticated port. No browser-direct network, CDN, clipboard API, web-link addon, iframe sandbox relaxation, or extra permission is required. OSC 52 clipboard sequences and OSC 8 hyperlinks are consumed without clipboard access or link activation.
22
+
23
+ The terminal descriptor uses `capabilities.streams:{version:1,purpose:"cc.qqxing/terminal",create:"cc.qqxing/terminal/v1/create",renew:"cc.qqxing/terminal/v1/renew",status:"cc.qqxing/terminal/v1/status",close:"cc.qqxing/terminal/v1/close",commands:{resize:"cc.qqxing/terminal/v1/resize"}}`, `scope:"chat"`, `methods:[]` and required streams/data v1. The manifest advertises streams, not terminal; terminal remains the business RPC namespace and data purpose. Lifecycle and command methods must not be ordinary App RPCs.
24
+
25
+ The shared streamOpen/write/command/read/close envelopes, `/api/mcp/app-stream` WS (`chat-app-stream-v1`), cursor/ACK/EOF/final-flush rules, budgets, ownership, presence/leases and single-runtime deployment limits are defined once in `chat-ai/docs/protocol/streams.md`, not by this terminal app.
26
+
27
+ The app-local [`stream-adapter.ts`](../src/terminal/ui/stream-adapter.ts) passes `{rootId?,cols,rows}` to streamOpen and maps resize to streamCommand. It encodes UTF-8 input as Base64, decodes output Base64 to Uint8Array and feeds xterm; only after xterm's write callback does it read-next/ACK. It interprets metadata.path/shell/exitCode; the generic host does not interpret PTY fields or decode UTF-8. On STREAM_DRAINING the UI disables new interaction/discards unsent input but continues read/ACK to the final frame.
28
+
29
+ node-pty can lose tail output when exit races paused reads. Staff reports detected risk as `error:{code:"OUTPUT_INCOMPLETE",message}`. The UI adapter maps generic ended to its local exited state, metadata.exitCode to the exit display and incomplete to **输出可能不完整**; read rejection displays **输出完整性无法确认**. The warning persists through later lifecycle changes, including exit code 0, and preserves displayed content. These local names are not bridge fields; diagnostics cannot recover lost bytes or guarantee completeness, and absent warnings/EOF/exit 0 do not prove complete history.
30
+
31
+ Input requests are sequential and small inputs are coalesced within bounded pending storage, each request at most 32 KiB UTF-8 with code-point-safe splitting. Pending UI input including the in-flight request is bounded at 256 KiB; overflow stops the instance instead of silently dropping/replaying input. Backend deliveries can be 64 KiB; staff splits them into ordered, awaited fd writes of at most 32 KiB without decoding arbitrary byte boundaries. Input consumption ACK follows complete fd writes; output ACK follows actual xterm rendering, never fetch/socket enqueue. The common 256 KiB sliding ACK window permits multiple backend blocks in flight, not per-block stop-and-wait; final EOF awaits receiver.end and its ACK, while interruption fails visibly and releases blocked work. Input acknowledgement does not prove command execution.
32
+
33
+ Presence and lease scheduling belong to the generic host stream contract, not an iframe timer. Staff retains its 30-second default remote lease. Hidden views keep output consumption active; BFCache/freeze does not guarantee indefinite PTY survival after presence or pending-consumption deadlines expire. PTY exit/draining is distinct from revocation or expiry; the UI retains read/ACK until the final frame rather than treating normal exit as loss of authorization.
34
+
35
+ ResizeObserver fits the local xterm viewport; remote resizes are debounced and ordered. Local display scrollback retains 2000 lines and is not a complete audit log. Terminal output is rendered by xterm, not inserted as HTML. Accessibility mode and the host light/dark semantic palette are enabled.
36
+
37
+ ## Building
38
+
39
+ With the project's `@xterm/xterm` and `@xterm/addon-fit` dependencies installed:
40
+
41
+ ```sh
42
+ npx tsc --project tsconfig.ui.json
43
+ node scripts/build-file-manager.mjs
44
+ ```
45
+
46
+ The script retains the file-manager build and additionally emits the terminal Resource. It rejects terminal HTML larger than **4 MiB** and embeds both xterm packages' complete LICENSE notices, including dependency copyright, inside the HTML. It does not require external assets at runtime.
47
+
48
+ For a terminal-only UI check without compiling the server, use `node scripts/build-file-manager.mjs --app terminal --outdir /path/to/scratch`. The output is `/path/to/scratch/terminal/ui/index.html`. App registration, extension negotiation, native PTY packaging, host authorization, and browser/remote lifecycle testing are separate integration responsibilities; a successful UI bundle alone does not verify them.
49
+
50
+
51
+ ## Host adapter boundary
52
+
53
+ The terminal descriptor uses `bridgeVersion:1`, generic `scope:"chat"` and streams capability; nonterminal leased-byte Apps reuse the same host modules. All iframe handshake/message envelopes retain version 1; the 2.0.0 extension contract requires matching host assets and offers no legacy compatibility path. PTY state, actual lease and process cleanup remain server-owned. The host generic stream module follows each returned positive leaseMs rather than requiring a 30-second maximum; staff retains its own 30-second default. Host browser presence and render-buffer budgets remain independent and do not extend the server's resource without a real renew request. The bridge boundary does not change the 2.0.0 reverse/raw data framing; it does not provide compatibility with pre-2.0.0 reverse handshakes.
54
+
55
+ ### HTTP idle policy
56
+
57
+ A live owned PTY, pending creation, output/ACK draining and TERM-to-KILL/exit waiting protect its HTTP session even when there is no new output. The existing terminal lease and data-consumption deadlines still govern abandonment; HTTP idle sweeping does not renew them. Finished records alone do not pin a session. A terminal iframe without a PTY is not activity. The listener policy does not apply to stdio or reverse sessions; see the README for defaults and configuration.
58
+
59
+
60
+ ### Business adapter and SDK boundary
61
+
62
+ `src/terminal/extension.ts` interprets the generic envelopes: create `{operationId,params:{rootId?,cols,rows}}` returns `{resourceId,leaseMs,metadata:{rootId,path,shell}}`; renew `{operationId,resourceId}` verifies the binding and returns `{resourceId,leaseMs}`; status `{operationId}` maps actual PTY state to active/closed and closed exit code to metadata; close `{operationId,reason:"user"}` returns an opaque object; resize uses `{resourceId,params:{cols,rows}}`. Dimension validation remains 2..1000 in staff, as do PTY state/cleanup and quotas (two active PTYs/session, 128 operation records, five-minute final-record retention). Internal TerminalSession contracts are unchanged. The UI adapter encodes input UTF-8 to Base64 and interprets metadata.path/shell/exitCode; host has no PTY business interpretation.
63
+
64
+ Host stream ownership, two per-owner slots including creation, single reader, authorization, presence/lease deadlines and late-create cleanup are generic. New Apps reuse those supported capabilities without host-core business branches. New host permissions/capabilities require explicit composition-root registration, validation and authorization; descriptors alone cannot grant them.
65
+
66
+ Both reverse transport modules can later join MCP Client (host)/Server (remote) SDKs, but no SDK split is done now. They only depend on SDK/standard libraries and injected transport parameters. `src/reverse/http.ts` HTTP/TLS/proxy policy, `src/transports/reverse.ts` factory/extension/data setup and reconnect, and host auth/management remain integration; no data-protocol interception is hidden in transport.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staff-mcp",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ChineseAStar/staff-mcp.git"
@@ -11,40 +11,61 @@
11
11
  "staff-mcp": "./dist/src/index.js"
12
12
  },
13
13
  "files": [
14
- "dist",
14
+ "dist/src",
15
15
  "builtin-profiles",
16
+ "docs/file-manager.md",
17
+ "docs/terminal.md",
16
18
  "README.md",
17
19
  "README_zh.md",
18
- "LICENSE"
20
+ "LICENSE",
21
+ "CHANGELOG.md"
19
22
  ],
20
23
  "scripts": {
21
- "build": "tsc",
24
+ "build": "tsc && tsc --project tsconfig.ui.json && node scripts/build-file-manager.mjs",
22
25
  "start": "node dist/src/index.js",
23
26
  "dev": "tsx src/index.ts",
24
27
  "test": "node --test dist/tests/**/*.test.js",
25
28
  "prepublishOnly": "npm run build"
26
29
  },
27
30
  "dependencies": {
28
- "@modelcontextprotocol/sdk": "1.30.0",
31
+ "@modelcontextprotocol/sdk": "1.32.1",
29
32
  "@vscode/ripgrep": "1.18.0",
30
- "commander": "^12.1.0",
31
- "cors": "^2.8.5",
32
- "express": "^4.21.1",
33
- "js-yaml": "^4.1.1",
34
- "mcp-fte": "^1.0.2",
35
- "mcp-reverse": "1.4.0",
36
- "undici": "^6.22.0",
37
- "zod": "^3.23.8"
33
+ "commander": "^15.0.0",
34
+ "cors": "^2.8.6",
35
+ "express": "^5.2.1",
36
+ "js-yaml": "^5.4.2",
37
+ "undici": "^7.30.0",
38
+ "zod": "^4.6.5"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/cors": "^2.8.19",
41
- "@types/express": "^5.0.0",
42
- "@types/js-yaml": "^4.0.9",
43
- "@types/node": "^22.9.0",
44
- "tsx": "^4.19.2",
45
- "typescript": "^5.6.3"
42
+ "@types/express": "^5.0.6",
43
+ "@types/node": "^22.20.5",
44
+ "tsx": "^4.23.15",
45
+ "typescript": "^5.9.3",
46
+ "react": "19.1.1",
47
+ "react-dom": "19.1.1",
48
+ "codemirror": "6.0.2",
49
+ "@codemirror/lang-json": "6.0.2",
50
+ "@codemirror/lang-markdown": "6.3.3",
51
+ "@codemirror/state": "6.7.6",
52
+ "@codemirror/view": "6.43.15",
53
+ "@codemirror/commands": "6.11.1",
54
+ "@codemirror/search": "6.7.2",
55
+ "@codemirror/lang-html": "6.4.12",
56
+ "@codemirror/lang-javascript": "6.2.5",
57
+ "@codemirror/lang-css": "6.3.1",
58
+ "@types/react": "19.1.13",
59
+ "@types/react-dom": "19.1.9",
60
+ "esbuild": "0.27.3",
61
+ "prettier": "3.6.2",
62
+ "@xterm/addon-fit": "0.11.0",
63
+ "@xterm/xterm": "6.0.0"
46
64
  },
47
65
  "engines": {
48
- "node": ">=20"
66
+ "node": ">=22.22.2"
67
+ },
68
+ "optionalDependencies": {
69
+ "node-pty": "1.1.0"
49
70
  }
50
71
  }
@@ -1,13 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
- import { type FteServerConfig } from "mcp-fte";
4
- export type FteTransportFactory = (transport: Transport, config: FteServerConfig) => Transport;
5
- /**
6
- * Install FTE wrapping while preserving transport lifecycle semantics.
7
- *
8
- * mcp-reverse transports can close themselves before the MCP SDK calls the
9
- * wrapper's close() method. Without this bridge, FteServer's timer and transfer
10
- * sessions would survive every reconnect. The composed onclose handler clears
11
- * the SDK binding first, then closes the FTE wrapper exactly once.
12
- */
13
- export declare function installFteTransportLifecycle(server: McpServer, sandbox: string[], wrapTransport?: FteTransportFactory): void;
@@ -1,76 +0,0 @@
1
- import { FteServer } from "mcp-fte";
2
- /**
3
- * Install FTE wrapping while preserving transport lifecycle semantics.
4
- *
5
- * mcp-reverse transports can close themselves before the MCP SDK calls the
6
- * wrapper's close() method. Without this bridge, FteServer's timer and transfer
7
- * sessions would survive every reconnect. The composed onclose handler clears
8
- * the SDK binding first, then closes the FTE wrapper exactly once.
9
- */
10
- export function installFteTransportLifecycle(server, sandbox, wrapTransport = (transport, config) => FteServer.wrapTransport(transport, config)) {
11
- const originalConnect = server.connect.bind(server);
12
- server.connect = async (transport) => {
13
- const wrapped = wrapTransport(transport, { sandbox });
14
- const rawWrappedClose = wrapped.close.bind(wrapped);
15
- let closing = false;
16
- let closePromise = Promise.resolve();
17
- const closeWrappedOnce = () => {
18
- if (closing)
19
- return closePromise;
20
- closing = true;
21
- try {
22
- closePromise = Promise.resolve(rawWrappedClose());
23
- }
24
- catch (error) {
25
- closePromise = Promise.reject(error);
26
- }
27
- return closePromise;
28
- };
29
- // Any SDK-driven close must use the same idempotent cleanup path.
30
- wrapped.close = closeWrappedOnce;
31
- try {
32
- await originalConnect(wrapped);
33
- }
34
- catch (error) {
35
- await closeWrappedOnce().catch((cleanupError) => {
36
- console.error("[staff-mcp] Failed to clean up FTE transport after connect failure:", cleanupError);
37
- });
38
- // Some transports reject start() without firing onclose. Release only if
39
- // this exact wrapper is still bound: an asynchronous stale cleanup must
40
- // never clear a newer reconnect that has already succeeded.
41
- if (server.server.transport === wrapped) {
42
- try {
43
- wrapped.onclose?.();
44
- }
45
- catch (cleanupError) {
46
- console.error("[staff-mcp] Failed to release MCP transport after connect failure:", cleanupError);
47
- }
48
- }
49
- throw error;
50
- }
51
- // Protocol.connect() installs its callbacks during originalConnect().
52
- // Compose only afterwards so both the SDK and FTE cleanup always run.
53
- const protocolOnClose = wrapped.onclose;
54
- let closeHandled = false;
55
- wrapped.onclose = () => {
56
- if (closeHandled)
57
- return;
58
- closeHandled = true;
59
- try {
60
- protocolOnClose?.();
61
- }
62
- finally {
63
- void closeWrappedOnce().catch((cleanupError) => {
64
- console.error("[staff-mcp] Failed to clean up closed FTE transport:", cleanupError);
65
- });
66
- }
67
- };
68
- // Cover the narrow race where this delegate closes after connect resolves
69
- // but before the composed callback above is installed. Transport identity,
70
- // rather than global connected state, protects a newer concurrent binding.
71
- if (server.server.transport !== wrapped) {
72
- await closeWrappedOnce();
73
- }
74
- };
75
- }
76
- //# sourceMappingURL=fte-lifecycle.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fte-lifecycle.js","sourceRoot":"","sources":["../../../src/transports/fte-lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAwB,MAAM,SAAS,CAAC;AAO1D;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAC1C,MAAiB,EACjB,OAAiB,EACjB,gBAAqC,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CACzD,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;IAE5C,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEpD,MAAM,CAAC,OAAO,GAAG,KAAK,EAAE,SAAoB,EAAiB,EAAE;QAC7D,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACtD,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEpD,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,YAAY,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;QACpD,MAAM,gBAAgB,GAAG,GAAkB,EAAE;YAC3C,IAAI,OAAO;gBAAE,OAAO,YAAY,CAAC;YACjC,OAAO,GAAG,IAAI,CAAC;YAEf,IAAI,CAAC;gBACH,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,YAAY,CAAC;QACtB,CAAC,CAAC;QAEF,kEAAkE;QAClE,OAAO,CAAC,KAAK,GAAG,gBAAgB,CAAC;QAEjC,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,EAAE;gBAC9C,OAAO,CAAC,KAAK,CAAC,qEAAqE,EAAE,YAAY,CAAC,CAAC;YACrG,CAAC,CAAC,CAAC;YAEH,yEAAyE;YACzE,wEAAwE;YACxE,4DAA4D;YAC5D,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;gBACxC,IAAI,CAAC;oBACH,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;gBACtB,CAAC;gBAAC,OAAO,YAAY,EAAE,CAAC;oBACtB,OAAO,CAAC,KAAK,CAAC,oEAAoE,EAAE,YAAY,CAAC,CAAC;gBACpG,CAAC;YACH,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QAED,sEAAsE;QACtE,sEAAsE;QACtE,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;QACxC,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE;YACrB,IAAI,YAAY;gBAAE,OAAO;YACzB,YAAY,GAAG,IAAI,CAAC;YAEpB,IAAI,CAAC;gBACH,eAAe,EAAE,EAAE,CAAC;YACtB,CAAC;oBAAS,CAAC;gBACT,KAAK,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,EAAE;oBAC7C,OAAO,CAAC,KAAK,CAAC,sDAAsD,EAAE,YAAY,CAAC,CAAC;gBACtF,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC;QAEF,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;YACxC,MAAM,gBAAgB,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}