@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.131
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.
- package/README.md +130 -10
- package/bin/commhub.ts +99 -11
- package/package.json +7 -7
- package/src/ack-create-request-transport.test.ts +182 -0
- package/src/ack-create-request.test.ts +50 -7
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +659 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-groups-perf.test.ts +223 -0
- package/src/agent-notice.ts +48 -0
- package/src/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams-http.test.ts +323 -0
- package/src/agent-teams.ts +244 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-validate.test.ts +5 -1
- package/src/auth-weak-password-login.test.ts +176 -0
- package/src/auth.ts +598 -49
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/binding-generation-http.test.ts +121 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/clone-node.test.ts +248 -0
- package/src/clone-node.ts +289 -0
- package/src/codex-config-flags.ts +77 -0
- package/src/codex-fork-contract.ts +23 -0
- package/src/codex-fork-migration.test.ts +38 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +107 -0
- package/src/config-apply-validate.test.ts +134 -1
- package/src/config-apply-validate.ts +111 -5
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-network-id.test.ts +79 -0
- package/src/create-node-tool-schema.test.ts +48 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +193 -7
- package/src/create-node-validate.ts +227 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -23
- package/src/daemon-provider-snapshots.test.ts +421 -0
- package/src/daemon-provider-snapshots.ts +327 -0
- package/src/daemon-token-owner-http.test.ts +236 -0
- package/src/dashboard-slash-routing-http.test.ts +107 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +933 -36
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +330 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +260 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-provider-config.test.ts +139 -0
- package/src/hub-provider-config.ts +105 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-caller.ts +49 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2362
- package/src/lifecycle-guard.ts +1 -1
- package/src/mcp-context-cost-http.test.ts +123 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +432 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +210 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +276 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-alias-control.ts +12 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-caller-identity-http.test.ts +781 -0
- package/src/node-create-request-status-http.test.ts +105 -0
- package/src/node-daemon-bindings.test.ts +132 -0
- package/src/node-daemon-bindings.ts +130 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-codex-login.test.ts +120 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +101 -0
- package/src/node-id-alias.test.ts +190 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +53 -0
- package/src/node-lifecycle-read-http.test.ts +245 -0
- package/src/node-lifecycle-read.ts +121 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +285 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-ownership.ts +51 -0
- package/src/node-token-privilege-http.test.ts +158 -0
- package/src/observer-avatar-http.test.ts +431 -0
- package/src/observer-push.test.ts +252 -0
- package/src/opencode-v2-catalog.test.ts +268 -0
- package/src/opencode-v2-catalog.ts +352 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/project-dir-warn-clear-http.test.ts +132 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +22 -2
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-handover-capabilities-http.test.ts +124 -0
- package/src/report-status-host-schema.test.ts +235 -0
- package/src/report-status-resume-id-handover.test.ts +150 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +620 -0
- package/src/requirement-due-reminders.ts +416 -0
- package/src/requirement-events-http.test.ts +268 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-abandoned-http.test.ts +236 -0
- package/src/requirements-comment-http.test.ts +167 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +349 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +259 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +288 -0
- package/src/requirements-stats.ts +167 -0
- package/src/requirements-status-participants-http.test.ts +222 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1806 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +130 -0
- package/src/rest-identity.ts +139 -0
- package/src/rest-projections.ts +68 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +76 -0
- package/src/retention.ts +15 -3
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/runtime-readiness.ts +90 -0
- package/src/schedule-agent-mcp-http.test.ts +381 -0
- package/src/schedule-agent.ts +192 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
- package/src/scheduled-failures-http.test.ts +233 -0
- package/src/scheduled-failures.ts +179 -0
- package/src/scheduled-offline.test.ts +131 -0
- package/src/scheduled-offline.ts +66 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +238 -0
- package/src/scheduled-stuck.ts +153 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +766 -0
- package/src/scoped-secrets.ts +124 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5257 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/session-token-ownership-http.test.ts +122 -0
- package/src/shared/codex-inventory.ts +41 -0
- package/src/shared/daemon-provider-login.ts +31 -0
- package/src/shared/daemon-provider-snapshot.ts +52 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/node-name.test.ts +62 -0
- package/src/shared/node-name.ts +161 -0
- package/src/shared/provider-application.ts +29 -0
- package/src/shared/provider-collection.ts +14 -0
- package/src/shared/provider-initialization.ts +8 -0
- package/src/shared/reserved-env-drift.test.ts +10 -0
- package/src/shared/scoped-secret-contract.ts +5 -0
- package/src/shared/task-content-limit-drift.test.ts +28 -0
- package/src/shared/task-content-limit.ts +25 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/start-node.test.ts +264 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +509 -1
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-auth-origin-http.test.ts +93 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +315 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-late-replies.ts +54 -0
- package/src/task-late-reply-http.test.ts +152 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +140 -0
- package/src/task-orphan-http.test.ts +287 -0
- package/src/task-orphan.ts +119 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +236 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-rejection-log-http.test.ts +160 -0
- package/src/task-rejection-log.ts +49 -0
- package/src/task-sender-http.test.ts +143 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +220 -0
- package/src/tool-audience.ts +118 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +3940 -325
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.ts +103 -6
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- package/src/user-inbox-schema.test.ts +54 -0
package/src/lifecycle-guard.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// PR1.2a (#346 follow-up) extracts the helper to this module so both
|
|
14
14
|
// the MCP tools and the REST handlers can import the same code path,
|
|
15
|
-
// per
|
|
15
|
+
// per team rule (grep every write site before adding a guard): any SQL-level
|
|
16
16
|
// guard's helper lives at module scope so every write site
|
|
17
17
|
// (MCP + REST + internal db.ts helpers) can use it.
|
|
18
18
|
//
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// #476(#469 MCP 审计的最后一项)—— Hub MCP 面的上下文成本。
|
|
2
|
+
//
|
|
3
|
+
// 钉住:
|
|
4
|
+
// 1. tools/list 的总字节有上限(TOOLS_LIST_MAX_BYTES):描述 / 参数说明不能悄悄长回去。每个工具也有上限,
|
|
5
|
+
// 单条工具描述有上限。要加新工具 / 新说明,改这里的数字 —— 改的时候就会被看见。
|
|
6
|
+
// 2. requirements_events 不带 requirement_id(全网动态)默认 50 条、从新到旧、next_cursor 翻更早的;
|
|
7
|
+
// 带 requirement_id 的单卡调用不变(默认 200);REST /api/requirements/events 不变(默认 200,App 用它)。
|
|
8
|
+
// 3. requirements_events 参数严格:不认识的参数 → -32602,列出能用的参数。
|
|
9
|
+
//
|
|
10
|
+
// 跑法:cd server && COMMHUB_DB=/tmp/x.db bun test src/mcp-context-cost-http.test.ts
|
|
11
|
+
// PG:COMMHUB_TEST_PG_URL=… COMMHUB_PG_EXPERIMENTAL=1(tests/test2123-hub-postgres-ladder 里注册)
|
|
12
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
14
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { register } from "./auth.js";
|
|
17
|
+
import { registerTools } from "./tools.js";
|
|
18
|
+
|
|
19
|
+
const activeDbPath = process.env.COMMHUB_DB ?? (process.env.COMMHUB_TEST_PG_URL ? "postgres" : undefined);
|
|
20
|
+
if (!activeDbPath) throw new Error("mcp-context-cost requires COMMHUB_DB (or COMMHUB_TEST_PG_URL) before module import");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* tools/list 整份 JSON 的字节上限。#476 之前(main,含 #475)74 个工具 76,824 B;修剪后 71,399 B。
|
|
24
|
+
* #478 起 tools/list 按调用者给:这里的调用者是用户令牌,看到 56 个工具 54,780 B(不含节点 / 协议工具)。
|
|
25
|
+
* 上限跟着收到用户这一档(~1.7 KB 余量);节点 / 全部工具的上限在 tool-audience-http.test.ts。
|
|
26
|
+
* 加工具或加说明超过它,就在这里改数字并在 PR 里说为什么。
|
|
27
|
+
*/
|
|
28
|
+
// Board #625: two human adoption tools; measured 58 tools / 56,615 B.
|
|
29
|
+
// Hub #938 clone_node is audience both (60 tools / 58,169 B). OpenCode V2
|
|
30
|
+
// catalog adds read_opencode_catalog and get_opencode_catalog on that user
|
|
31
|
+
// list. Measured together: 62 tools / 59,409 B. Ceiling keeps ~401 B.
|
|
32
|
+
const TOOLS_LIST_MAX_BYTES = 59_810;
|
|
33
|
+
/** 单个工具(名字 + 描述 + 输入 schema)的上限;最大的是 report_status(节点心跳,~8.4 KB 的遥测结构)。 */
|
|
34
|
+
const TOOL_MAX_BYTES = 9_000;
|
|
35
|
+
/** 单条工具描述的上限(说明写进参数的 describe,或者拆短)。 */
|
|
36
|
+
const DESCRIPTION_MAX_BYTES = 1_000;
|
|
37
|
+
const NETWORK_EVENTS = 80;
|
|
38
|
+
|
|
39
|
+
let server: any;
|
|
40
|
+
let base = "";
|
|
41
|
+
let token = "", netId = "";
|
|
42
|
+
let close: () => Promise<void>;
|
|
43
|
+
let client: Client;
|
|
44
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
45
|
+
const r: any = await client.callTool({ name, arguments: args });
|
|
46
|
+
const text = String(r.content?.[0]?.text ?? "");
|
|
47
|
+
return r.isError ? { mcpError: true, text } : { ...JSON.parse(text), _bytes: Buffer.byteLength(text, "utf8") };
|
|
48
|
+
};
|
|
49
|
+
let busyId = "";
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
const u = register(`ctx_${Date.now()}`, "CtxCost123!", undefined, "seed");
|
|
53
|
+
token = u.token!; netId = u.network_id!;
|
|
54
|
+
const s = new McpServer({ name: "ctx", version: "1" });
|
|
55
|
+
registerTools(s, undefined, null, u.user!.user_id, u.user!.username, false, "tok_user");
|
|
56
|
+
client = new Client({ name: "ctx-client", version: "1" });
|
|
57
|
+
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
58
|
+
await s.connect(st);
|
|
59
|
+
await client.connect(ct);
|
|
60
|
+
close = async () => { await client.close(); await s.close(); };
|
|
61
|
+
const mod: any = await import("./server.js");
|
|
62
|
+
server = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
63
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
64
|
+
// 一张卡上 60 条评论(单卡调用要能拿到 > 50 条),再加 20 张卡各一条
|
|
65
|
+
const busy = await call("requirements_create", { network_id: netId, name: "busy task" });
|
|
66
|
+
busyId = busy.requirement.id;
|
|
67
|
+
for (let i = 0; i < 60; i++) expect((await call("requirements_comment", { network_id: netId, id: busyId, text: `进展 ${i}:这一步完成了` })).ok).toBe(true);
|
|
68
|
+
for (let i = 0; i < NETWORK_EVENTS - 60; i++) {
|
|
69
|
+
const r = await call("requirements_create", { network_id: netId, name: `task ${i}` });
|
|
70
|
+
await call("requirements_comment", { network_id: netId, id: r.requirement.id, text: "已开始" });
|
|
71
|
+
}
|
|
72
|
+
}, 120_000);
|
|
73
|
+
afterAll(async () => { await close?.(); server?.stop?.(true); });
|
|
74
|
+
|
|
75
|
+
describe("#476 tools/list size", () => {
|
|
76
|
+
test("total, per tool and per description stay under their ceilings", async () => {
|
|
77
|
+
const { tools } = await client.listTools();
|
|
78
|
+
const total = Buffer.byteLength(JSON.stringify({ tools }), "utf8");
|
|
79
|
+
const sizes = tools.map((t: any) => ({ name: t.name, all: Buffer.byteLength(JSON.stringify(t)), desc: Buffer.byteLength(t.description ?? "") }));
|
|
80
|
+
sizes.sort((a, b) => b.all - a.all);
|
|
81
|
+
console.log(`tools/list: ${tools.length} tools, ${total} B; largest ${sizes.slice(0, 3).map(x => `${x.name}=${x.all}`).join(", ")}`);
|
|
82
|
+
expect(total).toBeLessThanOrEqual(TOOLS_LIST_MAX_BYTES);
|
|
83
|
+
for (const x of sizes) {
|
|
84
|
+
expect(x.all, x.name).toBeLessThanOrEqual(TOOL_MAX_BYTES);
|
|
85
|
+
expect(x.desc, x.name).toBeLessThanOrEqual(DESCRIPTION_MAX_BYTES);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("#476 requirements_events defaults", () => {
|
|
91
|
+
test("network-wide: 50 newest by default, next_cursor pages older events", async () => {
|
|
92
|
+
const p1 = await call("requirements_events", { network_id: netId });
|
|
93
|
+
expect(p1.events.length).toBe(50);
|
|
94
|
+
expect(typeof p1.next_cursor).toBe("string");
|
|
95
|
+
const times = p1.events.map((e: any) => e.at ?? e.created_at);
|
|
96
|
+
expect([...times].sort().reverse()).toEqual(times);
|
|
97
|
+
const p2 = await call("requirements_events", { network_id: netId, cursor: p1.next_cursor });
|
|
98
|
+
const ids = new Set([...p1.events, ...p2.events].map((e: any) => e.id));
|
|
99
|
+
expect(ids.size).toBe(p1.events.length + p2.events.length);
|
|
100
|
+
console.log(`network events page of 50: ${p1._bytes} B`);
|
|
101
|
+
expect(p1._bytes).toBeLessThan(25_000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("an explicit limit still wins; one task's timeline keeps its old default (200)", async () => {
|
|
105
|
+
expect((await call("requirements_events", { network_id: netId, limit: 70 })).events.length).toBe(70);
|
|
106
|
+
const one = await call("requirements_events", { network_id: netId, requirement_id: busyId });
|
|
107
|
+
expect(one.events.filter((e: any) => e.kind === "comment").length).toBe(60);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("REST GET /api/requirements/events is unchanged (default 200)", async () => {
|
|
111
|
+
const res = await fetch(`${base}/api/requirements/events?network_id=${netId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
112
|
+
const body = await res.json() as any;
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
expect(body.events.length).toBeGreaterThan(50);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("unknown parameters → -32602 listing the valid ones", async () => {
|
|
118
|
+
const r = await call("requirements_events", { network_id: netId, task_id: busyId });
|
|
119
|
+
expect(r.mcpError).toBe(true);
|
|
120
|
+
expect(r.text).toContain("-32602");
|
|
121
|
+
expect(r.text).toContain("unknown parameter(s): task_id; valid parameters: network_id, requirement_id, since, limit, cursor");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// MCP projects_create / projects_update / requirements_events(app 任务页审计 2026-10-02 M3:Agent 能做人能做的)。
|
|
2
|
+
// HTTP 集成测试(真实 Bun.serve,私有端口,临时库)。三个工具都走 app 用的同一套 REST 处理(handleRequirementsRequest),
|
|
3
|
+
// 所以这里不另抄一份权限规则:对每类调用者(网络 owner、全部任务成员、仅相关任务成员、viewer、节点令牌)都同时调
|
|
4
|
+
// MCP 和 REST,断言状态码 / 结果逐项一致。再钉住正向行为(建 / 改名 / 归档 / 取消归档、读流水、"#N")和工具出现在 tools/list 里。
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
|
7
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import { register, createNetworkTokenForNode } from "./auth.js";
|
|
11
|
+
import { db } from "./db.js";
|
|
12
|
+
|
|
13
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-mcp-projects-events-"));
|
|
14
|
+
let BASE = "";
|
|
15
|
+
let hub: any = null;
|
|
16
|
+
const PW = "McpProjectsEventsPassw0rd!x";
|
|
17
|
+
let NET = "";
|
|
18
|
+
const U: Record<string, { token: string; id: string }> = {};
|
|
19
|
+
|
|
20
|
+
async function rest(token: string, method: string, path: string, body?: unknown) {
|
|
21
|
+
const res = await fetch(`${BASE}${path}`, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
let json: any = null;
|
|
24
|
+
try { json = JSON.parse(text); } catch {}
|
|
25
|
+
return { status: res.status, body: json };
|
|
26
|
+
}
|
|
27
|
+
async function rpc(token: string, method: string, params?: unknown): Promise<any> {
|
|
28
|
+
const res = await fetch(`${BASE}/mcp`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-03-26" },
|
|
31
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, ...(params === undefined ? {} : { params }) }),
|
|
32
|
+
});
|
|
33
|
+
const raw = await res.text();
|
|
34
|
+
const lines = raw.split("\n").filter(x => x.startsWith("data:"));
|
|
35
|
+
return lines.length ? JSON.parse(lines.at(-1)!.slice(5).trim()) : JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
async function mcp(token: string, name: string, args: Record<string, unknown>): Promise<any> {
|
|
38
|
+
const out = await rpc(token, "tools/call", { name, arguments: args });
|
|
39
|
+
return JSON.parse(out.result.content[0].text);
|
|
40
|
+
}
|
|
41
|
+
// MCP error bodies from the REST handler carry its status (requirementsCall). Anything else that isn't ok (e.g. a tool-level
|
|
42
|
+
// gate) is NOT a success — a body without `status` must never be read as the 2xx (that hid a gate in the first draft).
|
|
43
|
+
const mcpStatus = (r: any, okStatus: number) => (r?.ok === true || (r && r.ok === undefined && (r.project || r.events)) ? okStatus : typeof r?.status === "number" ? r.status : -1);
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
47
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
48
|
+
process.env.HOST = "127.0.0.1";
|
|
49
|
+
const a = register(`mpe_owner_${Date.now()}`, PW, undefined, "Owner");
|
|
50
|
+
expect(a.ok).toBe(true);
|
|
51
|
+
U.owner = { token: a.token!, id: a.user!.user_id };
|
|
52
|
+
NET = a.network_id!;
|
|
53
|
+
db.run("UPDATE users SET role = 'admin' WHERE user_id = ?1", [U.owner.id]);
|
|
54
|
+
const mod: any = await import("./server.js");
|
|
55
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
56
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
57
|
+
const mk = async (username: string, role: string) => {
|
|
58
|
+
expect((await rest(U.owner.token, "POST", "/api/admin/users", { username, password: PW, network_id: NET, role })).status).toBe(200);
|
|
59
|
+
const login = await rest("", "POST", "/api/auth/login", { username, password: PW });
|
|
60
|
+
expect(login.status).toBe(200);
|
|
61
|
+
return { token: login.body.token as string, id: login.body.user.user_id as string };
|
|
62
|
+
};
|
|
63
|
+
const stamp = Date.now();
|
|
64
|
+
U.member = await mk(`mpe_member_${stamp}`, "member");
|
|
65
|
+
U.scoped = await mk(`mpe_scoped_${stamp}`, "member");
|
|
66
|
+
U.viewer = await mk(`mpe_viewer_${stamp}`, "viewer");
|
|
67
|
+
db.run("UPDATE network_members SET task_access = 'all' WHERE network_id = ?1 AND user_id = ?2", [NET, U.member.id]);
|
|
68
|
+
expect((await rest(U.owner.token, "PUT", `/api/networks/${NET}/members/${U.scoped.id}/task-grants`, { task_access: "scoped", project_grants: [] })).status).toBe(200);
|
|
69
|
+
const node = createNetworkTokenForNode(U.owner.id, NET, "mpe-node", "node_mpe");
|
|
70
|
+
U.node = { token: node.token!, id: "node_mpe" };
|
|
71
|
+
}, 30_000);
|
|
72
|
+
|
|
73
|
+
afterAll(() => {
|
|
74
|
+
try { hub?.stop?.(true); } catch {}
|
|
75
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("discoverable", () => {
|
|
79
|
+
test("tools/list carries the three tools with descriptions", async () => {
|
|
80
|
+
const list = await rpc(U.owner.token, "tools/list");
|
|
81
|
+
const byName = new Map((list.result.tools as any[]).map(t => [t.name, t]));
|
|
82
|
+
for (const n of ["projects_create", "projects_update", "requirements_events"]) {
|
|
83
|
+
expect(byName.has(n)).toBe(true);
|
|
84
|
+
expect(String(byName.get(n).description).length).toBeGreaterThan(40);
|
|
85
|
+
}
|
|
86
|
+
expect(String(byName.get("projects_list").description)).toContain("projects_create");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("projects_create / projects_update", () => {
|
|
91
|
+
test("owner: create, rename, recolour, archive, unarchive — same rows the app sees", async () => {
|
|
92
|
+
const created = await mcp(U.owner.token, "projects_create", { network_id: NET, name: "mpe-项目甲" });
|
|
93
|
+
expect(created.ok).toBe(true);
|
|
94
|
+
const id = created.project.id as string;
|
|
95
|
+
expect(created.project.name).toBe("mpe-项目甲");
|
|
96
|
+
const renamed = await mcp(U.owner.token, "projects_update", { network_id: NET, id, name: "mpe-项目乙", color: "#16a34a" });
|
|
97
|
+
expect(renamed.project.name).toBe("mpe-项目乙");
|
|
98
|
+
expect(renamed.project.color).toBe("#16a34a");
|
|
99
|
+
expect((await mcp(U.owner.token, "projects_update", { network_id: NET, id, archived: true })).project.archived).toBe(true);
|
|
100
|
+
const seen = await rest(U.owner.token, "GET", `/api/requirements/projects?network_id=${NET}`);
|
|
101
|
+
expect(seen.body.projects.find((p: any) => p.id === id).archived).toBe(true);
|
|
102
|
+
expect((await mcp(U.owner.token, "projects_update", { network_id: NET, id, archived: false })).project.archived).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("validation comes from the REST handler: duplicate name 409, unknown id 404", async () => {
|
|
106
|
+
await mcp(U.owner.token, "projects_create", { network_id: NET, name: "mpe-重名" });
|
|
107
|
+
const dup = await mcp(U.owner.token, "projects_create", { network_id: NET, name: "mpe-重名" });
|
|
108
|
+
expect(dup.ok).toBe(false);
|
|
109
|
+
expect(dup.status).toBe(409);
|
|
110
|
+
const missing = await mcp(U.owner.token, "projects_update", { network_id: NET, id: "proj_nobody", name: "x" });
|
|
111
|
+
expect(missing.status).toBe(404);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("permission parity with the app's REST path, for every kind of caller", async () => {
|
|
115
|
+
const results: Record<string, { mcp: number; rest: number }> = {};
|
|
116
|
+
for (const who of ["owner", "member", "scoped", "viewer", "node"]) {
|
|
117
|
+
const name = `mpe-parity-${who}`;
|
|
118
|
+
const viaMcp = await mcp(U[who].token, "projects_create", { network_id: NET, name: `${name}-mcp` });
|
|
119
|
+
const viaRest = await rest(U[who].token, "POST", `/api/requirements/projects?network_id=${NET}`, { name: `${name}-rest` });
|
|
120
|
+
results[who] = { mcp: mcpStatus(viaMcp, 201), rest: viaRest.status };
|
|
121
|
+
}
|
|
122
|
+
for (const r of Object.values(results)) expect(r.mcp).toBe(r.rest);
|
|
123
|
+
// the rule itself (RFC-038 §9): task-scoped members and viewers can't manage projects; the others can
|
|
124
|
+
expect(results.owner.rest).toBe(201);
|
|
125
|
+
expect(results.member.rest).toBe(201);
|
|
126
|
+
expect(results.scoped.rest).toBe(403);
|
|
127
|
+
expect(results.viewer.rest).toBe(403);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("requirements_events", () => {
|
|
132
|
+
test("one task's timeline by id and by #N, same rows as GET /api/requirements/events", async () => {
|
|
133
|
+
const card = await rest(U.owner.token, "POST", "/api/requirements", { network_id: NET, name: "mpe-流水示例" });
|
|
134
|
+
expect(card.status).toBe(201);
|
|
135
|
+
const id = card.body.requirement.id as string, seq = card.body.requirement.seq as number;
|
|
136
|
+
expect((await rest(U.owner.token, "PATCH", `/api/requirements/${id}?network_id=${NET}`, { priority: "high" })).status).toBe(200);
|
|
137
|
+
const viaMcp = await mcp(U.owner.token, "requirements_events", { network_id: NET, requirement_id: id });
|
|
138
|
+
const viaRest = await rest(U.owner.token, "GET", `/api/requirements/events?network_id=${NET}&requirement_id=${id}`);
|
|
139
|
+
expect(viaMcp.ok).toBe(true);
|
|
140
|
+
expect(viaMcp.events.length).toBeGreaterThan(0);
|
|
141
|
+
expect(viaMcp.events.map((e: any) => e.id)).toEqual(viaRest.body.events.map((e: any) => e.id));
|
|
142
|
+
expect(viaMcp.events.every((e: any) => e.requirement_id === id)).toBe(true);
|
|
143
|
+
const bySeq = await mcp(U.owner.token, "requirements_events", { network_id: NET, requirement_id: `#${seq}` });
|
|
144
|
+
expect(bySeq.events.map((e: any) => e.id)).toEqual(viaMcp.events.map((e: any) => e.id));
|
|
145
|
+
// paging / bad input come from the REST handler too
|
|
146
|
+
expect((await mcp(U.owner.token, "requirements_events", { network_id: NET, since: "not-a-date" })).status).toBe(400);
|
|
147
|
+
expect((await mcp(U.owner.token, "requirements_events", { network_id: NET, requirement_id: "#999999" })).status).toBe(404);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("visibility parity: a task-scoped member doesn't get events for a task they can't see", async () => {
|
|
151
|
+
const hidden = await rest(U.owner.token, "POST", "/api/requirements", { network_id: NET, name: "mpe-看不见的" });
|
|
152
|
+
const id = hidden.body.requirement.id as string;
|
|
153
|
+
await rest(U.owner.token, "PATCH", `/api/requirements/${id}?network_id=${NET}`, { priority: "low" });
|
|
154
|
+
for (const who of ["scoped", "member", "node"]) {
|
|
155
|
+
const viaMcp = await mcp(U[who].token, "requirements_events", { network_id: NET, requirement_id: id });
|
|
156
|
+
const viaRest = await rest(U[who].token, "GET", `/api/requirements/events?network_id=${NET}&requirement_id=${id}`);
|
|
157
|
+
expect((viaMcp.events ?? []).map((e: any) => e.id)).toEqual((viaRest.body?.events ?? []).map((e: any) => e.id));
|
|
158
|
+
}
|
|
159
|
+
expect((await mcp(U.scoped.token, "requirements_events", { network_id: NET, requirement_id: id })).events ?? []).toHaveLength(0);
|
|
160
|
+
expect((await mcp(U.member.token, "requirements_events", { network_id: NET, requirement_id: id })).events.length).toBeGreaterThan(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// POST /mcp in /api/stats/routes carries the JSON-RPC method and, for tools/call, the tool name
|
|
2
|
+
// ("POST /mcp tools/call report_status"). Production showed POST /mcp as the biggest route (15 req/s,
|
|
3
|
+
// ~13% of a core) with no way to tell which tool. This suite pins, over real HTTP:
|
|
4
|
+
// - the labels (tools/call <name>, tools/list, batch) and that MCP responses are unchanged in kind;
|
|
5
|
+
// - arguments never reach the stats (a marker in arguments / a hostile tool name never shows up);
|
|
6
|
+
// - odd bodies still get the transport's own answers (invalid JSON → -32700, oversized → parsed by
|
|
7
|
+
// the transport itself) and are labelled "?";
|
|
8
|
+
// - unauthenticated /mcp stays plain "POST /mcp" (the body is never read before auth).
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-mcp-route-label-"));
|
|
16
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
17
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
18
|
+
|
|
19
|
+
const PW = "McpRouteLabelPassw0rd!";
|
|
20
|
+
const MARKER = "zz-argument-marker-7f3a";
|
|
21
|
+
let BASE = "";
|
|
22
|
+
let hub: any = null;
|
|
23
|
+
let admin = "";
|
|
24
|
+
|
|
25
|
+
const MCP_HEADERS = { "Content-Type": "application/json", Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-03-26" };
|
|
26
|
+
async function mcpRaw(token: string, body: string): Promise<{ status: number; text: string }> {
|
|
27
|
+
const res = await fetch(`${BASE}/mcp`, { method: "POST", headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...MCP_HEADERS }, body });
|
|
28
|
+
return { status: res.status, text: await res.text() };
|
|
29
|
+
}
|
|
30
|
+
function rpc(text: string): any {
|
|
31
|
+
const lines = text.split("\n").filter(x => x.startsWith("data:"));
|
|
32
|
+
return lines.length ? JSON.parse(lines.at(-1)!.slice(5).trim()) : JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
async function routes(): Promise<{ text: string; keys: string[]; rows: any[] }> {
|
|
35
|
+
const res = await fetch(`${BASE}/api/stats/routes?minutes=5`, { headers: { Authorization: `Bearer ${admin}` } });
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
const rows = JSON.parse(text).routes as any[];
|
|
38
|
+
return { text, keys: rows.map(r => r.route), rows };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
process.env.HOST = "127.0.0.1";
|
|
43
|
+
const { register } = await import("./auth.js");
|
|
44
|
+
const { db } = await import("./db.js");
|
|
45
|
+
const a = register(`mrl_admin_${Date.now()}`, PW);
|
|
46
|
+
admin = a.token!;
|
|
47
|
+
db.run("UPDATE users SET role = 'admin' WHERE user_id = ?1", [a.user!.user_id]);
|
|
48
|
+
const mod: any = await import("./server.js");
|
|
49
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
50
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
51
|
+
}, 30_000);
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
try { hub?.stop?.(true); } catch {}
|
|
55
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("POST /mcp route label", () => {
|
|
59
|
+
test("tools/call is labelled with the tool name and still answers normally", async () => {
|
|
60
|
+
const r = await mcpRaw(admin, JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "get_all_status", arguments: { note: MARKER } } }));
|
|
61
|
+
expect(r.status).toBe(200);
|
|
62
|
+
expect(rpc(r.text).result).toBeDefined();
|
|
63
|
+
const { keys } = await routes();
|
|
64
|
+
expect(keys).toContain("POST /mcp tools/call get_all_status");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("tools/list and a batch get their own labels", async () => {
|
|
68
|
+
const list = await mcpRaw(admin, JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }));
|
|
69
|
+
expect(list.status).toBe(200);
|
|
70
|
+
expect(rpc(list.text).result.tools.length).toBeGreaterThan(10);
|
|
71
|
+
await mcpRaw(admin, JSON.stringify([{ jsonrpc: "2.0", id: 3, method: "tools/list" }]));
|
|
72
|
+
const { keys } = await routes();
|
|
73
|
+
expect(keys).toContain("POST /mcp tools/list");
|
|
74
|
+
expect(keys).toContain("POST /mcp batch");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("arguments and hostile tool names never reach the stats", async () => {
|
|
78
|
+
await mcpRaw(admin, JSON.stringify({ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: `x ${MARKER}`, arguments: { a: MARKER } } }));
|
|
79
|
+
await mcpRaw(admin, JSON.stringify({ jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "n".repeat(500), arguments: {} } }));
|
|
80
|
+
await mcpRaw(admin, JSON.stringify({ jsonrpc: "2.0", id: 6, method: `tools/${MARKER} with spaces` }));
|
|
81
|
+
const { text, keys } = await routes();
|
|
82
|
+
expect(text).not.toContain(MARKER);
|
|
83
|
+
expect(text).not.toContain("n".repeat(65));
|
|
84
|
+
expect(keys).toContain("POST /mcp tools/call ?");
|
|
85
|
+
expect(keys).toContain("POST /mcp ?");
|
|
86
|
+
for (const k of keys) expect(k.length).toBeLessThan(200);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("invalid JSON still gets the transport's parse error, labelled ?", async () => {
|
|
90
|
+
const r = await mcpRaw(admin, `{"jsonrpc":"2.0","method":"tools/list",`);
|
|
91
|
+
expect(r.status).toBe(400);
|
|
92
|
+
expect(rpc(r.text).error.code).toBe(-32700);
|
|
93
|
+
expect(rpc(r.text).error.message).toBe("Parse error: Invalid JSON");
|
|
94
|
+
const notRpc = await mcpRaw(admin, "null");
|
|
95
|
+
expect(notRpc.status).toBe(400);
|
|
96
|
+
expect(rpc(notRpc.text).error.code).toBe(-32700);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("a body over the label limit is left to the transport and still works, labelled ?", async () => {
|
|
100
|
+
const { __resetRouteTimingForTest } = await import("./route-timing.js");
|
|
101
|
+
__resetRouteTimingForTest();
|
|
102
|
+
const padded = JSON.stringify({ jsonrpc: "2.0", id: 7, method: "tools/list" }).replace("{", "{" + " ".repeat(1024 * 1024 + 10));
|
|
103
|
+
const r = await mcpRaw(admin, padded);
|
|
104
|
+
expect(r.status).toBe(200);
|
|
105
|
+
expect(rpc(r.text).result.tools.length).toBeGreaterThan(10);
|
|
106
|
+
const { keys } = await routes();
|
|
107
|
+
expect(keys).toContain("POST /mcp ?");
|
|
108
|
+
expect(keys).not.toContain("POST /mcp tools/list");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("unauthenticated /mcp is refused as before and not labelled", async () => {
|
|
112
|
+
const { __resetRouteTimingForTest } = await import("./route-timing.js");
|
|
113
|
+
__resetRouteTimingForTest();
|
|
114
|
+
const r = await mcpRaw("", JSON.stringify({ jsonrpc: "2.0", id: 8, method: "tools/list" }));
|
|
115
|
+
expect(r.status).toBe(401);
|
|
116
|
+
const { keys } = await routes();
|
|
117
|
+
expect(keys).toContain("POST /mcp");
|
|
118
|
+
expect(keys.filter(k => k.startsWith("POST /mcp "))).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|