@brightbeamai/chap-coordinator-mcp 0.3.3 → 0.3.5

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/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ AGENT_MCP_TOOLS: () => AGENT_MCP_TOOLS,
23
24
  META_CLIENT_CAPABILITIES: () => META_CLIENT_CAPABILITIES,
24
25
  META_CLIENT_INFO: () => META_CLIENT_INFO,
25
26
  META_PROTOCOL_VERSION: () => META_PROTOCOL_VERSION,
@@ -33,6 +34,7 @@ __export(src_exports, {
33
34
  TOOL_DESCRIPTIONS: () => TOOL_DESCRIPTIONS,
34
35
  TOOL_NAMES: () => TOOL_NAMES,
35
36
  UNSUPPORTED_PROTOCOL_VERSION: () => UNSUPPORTED_PROTOCOL_VERSION,
37
+ chapMcpHttpHandler: () => chapMcpHttpHandler,
36
38
  classifyEnvelope: () => classifyEnvelope,
37
39
  coerceToolArgs: () => coerceToolArgs,
38
40
  dispatchToolCall: () => dispatchToolCall,
@@ -42,7 +44,7 @@ __export(src_exports, {
42
44
  });
43
45
  module.exports = __toCommonJS(src_exports);
44
46
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
45
- var import_types2 = require("@modelcontextprotocol/sdk/types.js");
47
+ var import_types3 = require("@modelcontextprotocol/sdk/types.js");
46
48
  var import_zod = require("zod");
47
49
 
48
50
  // src/schemas.ts
@@ -110,6 +112,18 @@ var SCHEMAS = {
110
112
  enum: ["shadow", "trial", "production"],
111
113
  default: "production",
112
114
  description: "Highest mode a task in this workspace may request. A task.create above the ceiling is refused with -32040. The ceiling can be changed afterwards with chap.control.set_mode_ceiling."
115
+ },
116
+ join_policy: {
117
+ type: "string",
118
+ enum: ["open", "invite"],
119
+ default: "open",
120
+ description: "Who may join. 'open' admits anyone. 'invite' admits only a participant an admin invited with chap.workspace.invite, and the workspace's first member, which becomes its admin; any other join is refused with -32011. It cannot be changed afterwards."
121
+ },
122
+ review_rule: {
123
+ type: "string",
124
+ pattern: "^(any_one_approves|all_approve|quorum:[1-9][0-9]*)$",
125
+ default: "any_one_approves",
126
+ description: "The rule of every review that task.complete opens here: any_one_approves, all_approve or quorum:<n>. An author cannot open a review of their own review-required task, so this is how a workspace asks for more than one approval. It cannot be changed afterwards."
113
127
  }
114
128
  },
115
129
  additionalProperties: false
@@ -131,6 +145,26 @@ var SCHEMAS = {
131
145
  },
132
146
  required: ["workspace", "profiles"]
133
147
  },
148
+ "chap.workspace.invite": {
149
+ type: "object",
150
+ properties: {
151
+ workspace: WORKSPACE_ID,
152
+ from: { ...PARTICIPANT_URI, description: "The admin who invites. Any other sender is refused with -32011." },
153
+ uri: { ...PARTICIPANT_URI, description: "The participant invited. Its scheme must name type, as at a join." },
154
+ type: {
155
+ type: "string",
156
+ enum: ["human", "agent", "service"],
157
+ description: "The kind of participant invited, which the scheme of uri must name. The member takes this type. group: and workspace: URIs name address scopes, which cannot join, and are refused with -32602."
158
+ },
159
+ role: { type: "string", description: "The role the member takes when it joins, whatever role the join names. Only an invitation grants 'admin' after the first member, and only to a human or a service." },
160
+ jwks: {
161
+ type: "object",
162
+ description: "Keys to pin, as {keys: [JWK, ...]}, each with a kid. The join's keys must be among them, and the member holds them. For a member that holds no key in force, the call registers these keys at once: the way back for a member whose keys were all revoked."
163
+ },
164
+ oidc_sub: { type: "string", description: "The OIDC subject whose token admits the invitee: the join must carry an oidc_token for it. Needs a token verifier on the coordinator." }
165
+ },
166
+ required: ["workspace", "from", "uri", "type"]
167
+ },
134
168
  "chap.participant.join": {
135
169
  type: "object",
136
170
  properties: {
@@ -138,10 +172,10 @@ var SCHEMAS = {
138
172
  from: PARTICIPANT_URI,
139
173
  type: {
140
174
  type: "string",
141
- enum: ["human", "agent", "service", "group", "workspace"],
142
- description: "The kind of participant. Only members of type 'human' are eligible for the review that chap.task.complete opens on a task marked review_required, and a completion with no eligible human is refused with -32011."
175
+ enum: ["human", "agent", "service"],
176
+ description: "The kind of participant, which the scheme of from must name: 'agent:' for an agent, and so on. Any other join is refused with -32602, and so is a join of a group: or workspace: URI, which names an address scope. Only members of type 'human' are eligible for the review that chap.task.complete opens on a task marked review_required, and a completion with no eligible human is refused with -32011. In a workspace that requires signatures, a participant that signs joins with a key."
143
177
  },
144
- role: { type: "string", description: "Operator-defined role, e.g. 'reviewer' or 'drafter'. One value is read by the coordinator: 'admin' permits revoking another member's key." },
178
+ role: { type: "string", description: "Operator-defined role, e.g. 'reviewer' or 'drafter'. One value is read by the coordinator: 'admin', which changes the profiles, invites, and revokes another member's key. It is granted, never claimed: only the workspace's first member, or a participant an admin's invitation names as admin, may name it at a join, and any other join naming it is refused with -32011. Only a human or a service holds it. An invitation's role replaces the one named here." },
145
179
  display_name: { type: "string", description: "Human-readable name for interfaces. Not used in authorisation." }
146
180
  },
147
181
  required: ["workspace", "from", "type"]
@@ -257,6 +291,11 @@ var SCHEMAS = {
257
291
  },
258
292
  required: ["workspace", "from", "task_id", "to", "artefact"]
259
293
  },
294
+ "chap.review.acknowledge": {
295
+ type: "object",
296
+ properties: { workspace: WORKSPACE_ID, from: PARTICIPANT_URI, task_id: TASK_ID },
297
+ required: ["workspace", "from", "task_id"]
298
+ },
260
299
  "chap.decide.approve": {
261
300
  type: "object",
262
301
  properties: {
@@ -761,19 +800,21 @@ var TOOL_DESCRIPTIONS = {
761
800
  "chap.workspace.create": "Create a workspace: the container for the participants, tasks and audit log of one piece of collaborative work. Returns the workspace id, which every later call carries.",
762
801
  "chap.workspace.describe": "Report the current state of a workspace: its members, enabled profiles, audit length, and task and override counts.",
763
802
  "chap.workspace.set_profiles": "Replace the whole set of profiles enabled on a workspace. The list is not merged: any profile absent from it is dropped, so send the complete set you want. Enabling audit-scitt/1.0 on a workspace that already has entries starts the chain from that point and leaves the earlier entries permanently outside it, which chap.audit.verify_chain then reports as not_evaluated. Prefer setting profiles at chap.workspace.create, where no entries exist yet to strand.",
803
+ "chap.workspace.invite": "Invite a participant to a workspace, as its admin. The invitation names the type and role the member takes when it joins, and may pin its keys and its OIDC subject. A workspace whose join policy is 'invite' admits nobody else. For a member that holds no key in force, the call registers the keys it names at once, so a member whose keys were all revoked can sign again. Where a token or credential verifier is configured, an invitation names no keys for a person: their keys arrive with the token they join with (-32602).",
764
804
  "chap.participant.join": "Add a participant to a workspace. The type given, human, agent, service, group or workspace, decides whether they are eligible to review work that requires it.",
765
805
  "chap.participant.leave": "Remove a participant from a workspace. Entries they have already written stay in the audit log and their past signatures still verify, so leaving is not a way to withdraw a decision. They can no longer be assigned tasks or act as a reviewer, and returning means a fresh chap.participant.join.",
766
806
  "chap.task.create": "Create a task: a unit of work assigned to one participant. Set review_required to make completion depend on a reviewer decision rather than on the assignee. Returns a task_id that the rest of the lifecycle takes. Supplying idempotency_key is what makes a retry safe: a repeat carrying a key already seen returns the original task, while a repeat without one creates a second task. Assigning to a paused member is refused.",
767
- "chap.task.update": "Move a task to a new state. Only the transitions in the specification's lifecycle table are accepted, and a task that requires review cannot be completed here.",
807
+ "chap.task.update": "Move a task to a new state. Only the transitions in the specification's lifecycle table are accepted, and a task that requires review cannot be completed here. A task whose review no decision has closed, or a successor that carries one on, is not declined (-32602).",
768
808
  "chap.task.complete": "Submit a task's output. A task that requires review does not complete: the output is held as the artefact under review, the task moves to review_requested, and a reviewer decision completes it. Any other task completes immediately.",
769
809
  "chap.audit.read": "Read entries from a workspace's audit log, optionally within a sequence range and filtered by method, sender, task or outcome. This is the query surface for everything the workspace has recorded, decisions and overrides included. An accepted call is under 'envelope'. A refused attempt the coordinator recorded, such as a decision by someone the review was not addressed to, is under 'request' with an 'outcome' giving the error code, and it did not take effect. Reads only: it records nothing, so it never appears in its own output. Entries come back as recorded, without aggregation, so grouping them, by tag for instance, is the reader's job.",
770
810
  // review/1.0
771
- "chap.review.request": "Open a review on a task and address it to one or more reviewers. They then call chap.decide.approve, chap.decide.reject, chap.decide.override or chap.abstain.declare. Repeating the request with the same artefact adds reviewers to the open review.",
811
+ "chap.review.request": "Open a review on a task and address it to one or more reviewers. They then call chap.decide.approve, chap.decide.reject, chap.decide.override or chap.abstain.declare, and may first say they have begun with chap.review.acknowledge. Repeating the request with the same artefact adds reviewers to the open review. A task marked review_required is reviewed through chap.task.complete, which opens its review under the workspace's rule; on such a task this tool only adds people to a review already open, and before it opens is refused with -32011.",
812
+ "chap.review.acknowledge": "Say that this reviewer has begun the open review on a task, before deciding it. The call is recorded, so the audit log shows when each reviewer took the review up as well as when they decided. It decides nothing: the review stays open and the acknowledgement counts towards no rule. Only someone who may decide the review may send it, and anyone else is refused with -32011. A task with no open review is refused with -32010, and a second acknowledgement from the same reviewer in the same round with -32602. Returns requested_at, the time the round opened, which names it.",
772
813
  "chap.decide.approve": "Approve the artefact under review as it stands. Only a reviewer the review was addressed to may decide, and anyone else is refused with -32011. The task completes once the review's rule is satisfied, so under all_approve or quorum:N an approval may leave the review open awaiting others. Use chap.decide.override instead to accept a corrected version.",
773
814
  "chap.decide.reject": "Reject the artefact under review. The task is declined, or returns to in_progress when request_revision is set, which is the choice between ending the work and sending it back. Only a reviewer the review was addressed to may decide. Use chap.decide.override instead to correct the artefact and accept it rather than refuse it.",
774
815
  "chap.decide.override": "Correct the artefact under review with an RFC 6902 JSON Patch and accept the result. The patch, the rationale and any tags are recorded together, so the audit log holds what was changed and why, rather than only that the work was not accepted as written.",
775
816
  "chap.abstain.declare": "Stand aside from a review this participant cannot decide, for example on a conflict of interest or for want of context. Use it in place of chap.decide.reject, which judges the work; abstaining judges nothing and leaves the artefact intact. It requires an open review addressed to the caller and answers -32010 otherwise. The task moves to abstained, which is not terminal: chap.escalate.raise or a fresh chap.review.request can still carry it forward.",
776
- "chap.escalate.raise": "Hand a task upwards, where the current holder can act but should not decide alone. The original moves to escalated and is linked to a new task for whoever takes it on. Use chap.handoff.propose to pass work sideways without that link, and chap.control.supersede to replace a task outright. The successor starts with an empty input unless one is supplied, so restate whatever the new assignee needs. A completed, cancelled or superseded task cannot be escalated. The successor takes the mode new_task.mode names, or else the original's, and requires review when the original did, or when it is a trial task under modes/1.0. A paused assignee is refused with -32063, and a mode above the workspace's current ceiling with -32040; after the ceiling is lowered, name a mode within it.",
817
+ "chap.escalate.raise": "Hand a task upwards, where the current holder can act but should not decide alone. The original moves to escalated and is linked to a new task for whoever takes it on. Use chap.handoff.propose to pass work sideways without that link, and chap.control.supersede to replace a task outright. The successor starts with an empty input unless one is supplied, so restate whatever the new assignee needs. A completed, cancelled or superseded task cannot be escalated. The successor takes the mode new_task.mode names, or else the original's, and requires review when the original did or held a review, or when it is a trial task under modes/1.0. Whoever made the original's work counts as an author of the successor's review. A paused assignee is refused with -32063, and a mode above the workspace's current ceiling with -32040; after the ceiling is lowered, name a mode within it.",
777
818
  // whisper/1.0
778
819
  "chap.whisper.ask": "Put one question to one or more participants, with a deadline and a default. If the deadline passes unanswered the default applies, so a task is never blocked waiting on a reply.",
779
820
  "chap.whisper.answer": "Answer an open whisper. Where the question carried options, the answer must name one of their ids and any other value is refused with -32022. A whisper already answered is refused with -32020 and one past its deadline with -32021, in which case the default supplied at chap.whisper.ask has already been applied.",
@@ -789,10 +830,10 @@ var TOOL_DESCRIPTIONS = {
789
830
  // control/1.0
790
831
  "chap.control.pause": "Pause work. Scoped to a task it moves that task to paused; to a participant it stops new tasks being assigned to them; to the workspace it refuses every method except describing, reading the audit log, joining, leaving and resuming.",
791
832
  "chap.control.resume": "Resume work paused at the same scope: a task returns to the state it held at the pause, a participant can be assigned tasks again, a workspace returns to active.",
792
- "chap.control.cancel": "Cancel a task. Cancelled is terminal, and a task that has already settled cannot be cancelled.",
833
+ "chap.control.cancel": "Cancel a task. Cancelled is terminal, and a task that has already settled cannot be cancelled. From the moment a review the task requires opens until a reviewer rejects the work, the authors of the work cannot cancel it, even once they take the work back, nor a successor that carries its review (-32011).",
793
834
  "chap.control.snapshot": "Capture the workspace state as an artefact before a change you may want to undo. Returns a snapshot artefact id, which is the only thing chap.control.rollback accepts as a target; the label is for a human reading the log and cannot be used to select it. Taking a snapshot changes nothing. Note that a rollback restores only the mode ceiling and member roles, so a snapshot is a narrow safety net rather than a general undo.",
794
835
  "chap.control.rollback": "Restore workspace state from a snapshot. The mode ceiling and member roles are restored; the rollback is appended to the audit log rather than rewriting it.",
795
- "chap.control.supersede": "Replace a task with a successor in one call. The original moves to superseded and stays linked to its replacement. The successor requires review when the original did, whatever its own review_required says, and a paused assignee is refused with -32063.",
836
+ "chap.control.supersede": "Replace a task with a successor in one call. The original moves to superseded and stays linked to its replacement. The successor requires review when the original did, whatever its own review_required says, and whoever made the original's work counts as an author of the successor's review. A paused assignee is refused with -32063, and from the moment a review the original requires opens until a reviewer rejects the work, the authors of the work cannot supersede it, nor a successor that carries its review (-32011).",
796
837
  "chap.control.set_mode_ceiling": "Set the highest operating mode tasks in this workspace may request. A task above the ceiling is refused.",
797
838
  // routing/1.0
798
839
  "chap.task.route": "Choose an assignee for a task from a list of candidates and reassign the task to the one selected. Use it where the choice itself belongs on the record: it writes a route_decision artefact naming the policy, the candidate chosen and the alternatives passed over, none of which is captured by setting assignee directly on chap.task.create. The default policy takes the first candidate that is a workspace member and is not paused. An operator-supplied policy may choose on any basis, and the call is refused with -32510 when its choice is not a member, or with -32063 when its choice is paused.",
@@ -843,8 +884,10 @@ var IDEMPOTENT = /* @__PURE__ */ new Set([
843
884
  // pausing an already paused target changes nothing
844
885
  "chap.control.set_mode_ceiling",
845
886
  // setting the ceiling it already has changes nothing
846
- "chap.deliberate.close"
887
+ "chap.deliberate.close",
847
888
  // returns the same outcome, recomputing nothing
889
+ "chap.review.acknowledge"
890
+ // a second acknowledgement in the round is refused and records nothing
848
891
  ]);
849
892
  var OPEN_WORLD = /* @__PURE__ */ new Set([
850
893
  "chap.audit.submit_to_scitt",
@@ -854,6 +897,7 @@ var TITLES = {
854
897
  "chap.workspace.create": "Create workspace",
855
898
  "chap.workspace.describe": "Describe workspace",
856
899
  "chap.workspace.set_profiles": "Replace workspace profiles",
900
+ "chap.workspace.invite": "Invite participant",
857
901
  "chap.participant.join": "Add participant",
858
902
  "chap.participant.leave": "Remove participant",
859
903
  "chap.task.create": "Create task",
@@ -861,6 +905,7 @@ var TITLES = {
861
905
  "chap.task.complete": "Submit task output",
862
906
  "chap.audit.read": "Read audit log",
863
907
  "chap.review.request": "Request review",
908
+ "chap.review.acknowledge": "Acknowledge review",
864
909
  "chap.decide.approve": "Approve",
865
910
  "chap.decide.reject": "Reject",
866
911
  "chap.decide.override": "Override with a correction",
@@ -901,6 +946,19 @@ var TOOL_ANNOTATIONS = Object.fromEntries(
901
946
  openWorldHint: OPEN_WORLD.has(name)
902
947
  }])
903
948
  );
949
+ var AGENT_MCP_TOOLS = /* @__PURE__ */ new Set([
950
+ "chap.task.create",
951
+ "chap.task.update",
952
+ "chap.task.complete",
953
+ "chap.whisper.ask",
954
+ "chap.decide.approve",
955
+ "chap.decide.reject",
956
+ "chap.decide.override",
957
+ "chap.review.acknowledge",
958
+ "chap.workspace.describe",
959
+ "chap.audit.read",
960
+ "chap.audit.verify_chain"
961
+ ]);
904
962
 
905
963
  // src/envelope.ts
906
964
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -953,17 +1011,371 @@ function readMeta(params) {
953
1011
  return meta;
954
1012
  }
955
1013
 
1014
+ // src/remote.ts
1015
+ var ChapEndpointError = class extends Error {
1016
+ constructor(message) {
1017
+ super(message);
1018
+ this.name = "ChapEndpointError";
1019
+ }
1020
+ };
1021
+
1022
+ // src/http.ts
1023
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1024
+ var import_types2 = require("@modelcontextprotocol/sdk/types.js");
1025
+ var HEADER_MISMATCH = -32020;
1026
+ var DEFAULT_MAX_BODY_BYTES = 1048576 + 65536;
1027
+ var DEFAULT_TIMEOUT_MS = 45e3;
1028
+ var HANDSHAKE_VERSIONS = import_types2.SUPPORTED_PROTOCOL_VERSIONS.filter(
1029
+ (v) => !MODERN_PROTOCOL_VERSIONS.includes(v)
1030
+ );
1031
+ var MODERN_METHODS = /* @__PURE__ */ new Set(["server/discover", "tools/list", "tools/call"]);
1032
+ var ROUTING_HEADERS = /* @__PURE__ */ new Set(["mcp-protocol-version", "mcp-method", "mcp-name"]);
1033
+ var STATUS_FOR_CODE = /* @__PURE__ */ new Map([
1034
+ [import_types2.ErrorCode.ParseError, 400],
1035
+ [import_types2.ErrorCode.InvalidRequest, 400],
1036
+ [import_types2.ErrorCode.InvalidParams, 400],
1037
+ [HEADER_MISMATCH, 400],
1038
+ [UNSUPPORTED_PROTOCOL_VERSION, 400],
1039
+ [import_types2.ErrorCode.MethodNotFound, 404]
1040
+ ]);
1041
+ var NOT_ONE_MESSAGE = "Body must be a single JSON-RPC request or notification object";
1042
+ var INTERNAL = { code: import_types2.ErrorCode.InternalError, message: "Internal error" };
1043
+ function mcpHttpHandler(settings) {
1044
+ return async function handle(req, res, parsedBody) {
1045
+ try {
1046
+ const hosts = servedHosts(req, settings.allowedHosts);
1047
+ if (!hosts.has((req.headers.host ?? "").toLowerCase())) {
1048
+ answer(res, 421, { code: import_types2.ErrorCode.InvalidRequest, message: "Unknown host: add it to allowedHosts to serve under that name" });
1049
+ return;
1050
+ }
1051
+ if (foreignOrigin(req.headers.origin, hosts, settings.allowedOrigins)) {
1052
+ answer(res, 403, { code: import_types2.ErrorCode.InvalidRequest, message: "Cross-origin requests are refused: add the origin to allowedOrigins to serve it" });
1053
+ return;
1054
+ }
1055
+ if (req.method !== "POST") {
1056
+ res.writeHead(405, { allow: "POST" }).end();
1057
+ return;
1058
+ }
1059
+ const declared = req.headers["mcp-protocol-version"];
1060
+ if (declared === void 0 || typeof declared === "string" && HANDSHAKE_VERSIONS.includes(declared)) {
1061
+ await handshake(settings, req, res, parsedBody);
1062
+ return;
1063
+ }
1064
+ await singleExchange(settings, req, res, parsedBody);
1065
+ } catch (err) {
1066
+ console.error("MCP request failed:", err);
1067
+ if (!res.headersSent) answer(res, 500, INTERNAL);
1068
+ else res.destroy();
1069
+ }
1070
+ };
1071
+ }
1072
+ async function handshake(settings, req, res, parsedBody) {
1073
+ let body = parsedBody;
1074
+ if (body === void 0 && isJsonType(req.headers["content-type"])) {
1075
+ const read = await readJson(req, res, settings.maxBodyBytes);
1076
+ if (!read) return;
1077
+ body = read.value;
1078
+ }
1079
+ if (Array.isArray(body)) {
1080
+ answer(res, 400, { code: import_types2.ErrorCode.InvalidRequest, message: "A batch is not accepted: send each JSON-RPC message in a POST of its own" });
1081
+ return;
1082
+ }
1083
+ const header = req.headers["mcp-protocol-version"];
1084
+ const version = envelopeVersion(body);
1085
+ if (version !== void 0 && version !== header) {
1086
+ answer(res, 400, {
1087
+ code: HEADER_MISMATCH,
1088
+ message: header === void 0 ? "mcp-protocol-version header is missing, and the request envelope declares a protocol version" : "mcp-protocol-version header does not match the request envelope's protocol version"
1089
+ }, idOf(body));
1090
+ return;
1091
+ }
1092
+ const id = (0, import_types2.isJSONRPCRequest)(body) ? body.id : void 0;
1093
+ const server = settings.makeServer();
1094
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0, maxRequestBodySize: settings.maxBodyBytes });
1095
+ const timer = setTimeout(() => {
1096
+ console.error(`MCP request ${JSON.stringify(id ?? null)} had no answer within ${settings.timeoutMs} ms`);
1097
+ if (!res.headersSent) answer(res, 500, INTERNAL, id);
1098
+ else if (id !== void 0) transport.send({ jsonrpc: "2.0", id, error: INTERNAL }).catch(() => res.destroy());
1099
+ else res.destroy();
1100
+ }, settings.timeoutMs);
1101
+ res.on("close", () => {
1102
+ clearTimeout(timer);
1103
+ void transport.close();
1104
+ void server.close();
1105
+ });
1106
+ await server.connect(transport);
1107
+ await transport.handleRequest(req, res, body);
1108
+ }
1109
+ async function singleExchange(settings, req, res, parsedBody) {
1110
+ if (!acceptsJson(req.headers.accept)) {
1111
+ res.writeHead(406).end();
1112
+ return;
1113
+ }
1114
+ if (!isJsonType(req.headers["content-type"])) {
1115
+ answer(res, 415, { code: import_types2.ErrorCode.InvalidRequest, message: "Unsupported Media Type: Content-Type must be application/json" });
1116
+ return;
1117
+ }
1118
+ let body = parsedBody;
1119
+ if (body === void 0) {
1120
+ const read = await readJson(req, res, settings.maxBodyBytes);
1121
+ if (!read) return;
1122
+ body = read.value;
1123
+ }
1124
+ if (isObject(body) && !("id" in body)) {
1125
+ if (!(0, import_types2.isJSONRPCNotification)(body)) {
1126
+ answer(res, 400, { code: import_types2.ErrorCode.InvalidRequest, message: NOT_ONE_MESSAGE });
1127
+ return;
1128
+ }
1129
+ const version = String(req.headers["mcp-protocol-version"]);
1130
+ if (!MODERN_PROTOCOL_VERSIONS.includes(version)) {
1131
+ answer(res, 400, unsupported(version));
1132
+ return;
1133
+ }
1134
+ res.writeHead(202).end();
1135
+ return;
1136
+ }
1137
+ if (!(0, import_types2.isJSONRPCRequest)(body)) {
1138
+ answer(res, 400, { code: import_types2.ErrorCode.InvalidRequest, message: NOT_ONE_MESSAGE }, idOf(body));
1139
+ return;
1140
+ }
1141
+ const refusal = duplicatedHeader(req.rawHeaders) ?? classify(body, req.headers);
1142
+ if (refusal) {
1143
+ answer(res, STATUS_FOR_CODE.get(refusal.code) ?? 200, refusal, body.id);
1144
+ return;
1145
+ }
1146
+ if (!MODERN_METHODS.has(body.method)) {
1147
+ answer(res, 404, { code: import_types2.ErrorCode.MethodNotFound, message: "Method not found", data: body.method }, body.id);
1148
+ return;
1149
+ }
1150
+ if (!validParams(body)) {
1151
+ answer(res, 400, { code: import_types2.ErrorCode.InvalidParams, message: "Invalid request parameters" }, body.id);
1152
+ return;
1153
+ }
1154
+ const outcome = await exchange(settings, res, body);
1155
+ switch (outcome.kind) {
1156
+ case "reply": {
1157
+ const error = outcome.message.error;
1158
+ const status = error ? STATUS_FOR_CODE.get(error.code) ?? 200 : 200;
1159
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(outcome.message));
1160
+ return;
1161
+ }
1162
+ case "gone":
1163
+ return;
1164
+ case "timeout":
1165
+ console.error(`MCP request ${JSON.stringify(body.id)} had no answer within ${settings.timeoutMs} ms`);
1166
+ answer(res, 500, INTERNAL, body.id);
1167
+ return;
1168
+ case "error":
1169
+ console.error("MCP request failed:", outcome.error);
1170
+ answer(res, 500, INTERNAL, body.id);
1171
+ }
1172
+ }
1173
+ async function exchange(settings, res, request) {
1174
+ let settle;
1175
+ const outcome = new Promise((resolve) => {
1176
+ settle = resolve;
1177
+ });
1178
+ const timer = setTimeout(() => settle({ kind: "timeout" }), settings.timeoutMs);
1179
+ const gone = () => settle({ kind: "gone" });
1180
+ res.once("close", gone);
1181
+ const server = settings.makeServer();
1182
+ server.onerror = (error) => settle({ kind: "error", error });
1183
+ const transport = new SingleExchangeTransport((message) => settle({ kind: "reply", message }));
1184
+ try {
1185
+ await server.connect(transport);
1186
+ transport.onmessage?.(request);
1187
+ return await outcome;
1188
+ } finally {
1189
+ clearTimeout(timer);
1190
+ res.off("close", gone);
1191
+ await server.close();
1192
+ }
1193
+ }
1194
+ function classify(body, headers) {
1195
+ const meta = isObject(body.params) ? body.params._meta : void 0;
1196
+ if (!isObject(meta)) {
1197
+ return {
1198
+ code: import_types2.ErrorCode.InvalidParams,
1199
+ message: `params._meta must be an object carrying the required '${META_PROTOCOL_VERSION}' and '${META_CLIENT_CAPABILITIES}' envelope keys`
1200
+ };
1201
+ }
1202
+ const missing = [META_PROTOCOL_VERSION, META_CLIENT_CAPABILITIES].filter((key) => !(key in meta));
1203
+ if (missing.length) {
1204
+ return { code: import_types2.ErrorCode.InvalidParams, message: `params._meta is missing the required envelope key(s): ${missing.join(", ")}` };
1205
+ }
1206
+ const version = meta[META_PROTOCOL_VERSION];
1207
+ if (headers["mcp-protocol-version"] !== version) {
1208
+ return { code: HEADER_MISMATCH, message: "mcp-protocol-version header does not match the request envelope's protocol version" };
1209
+ }
1210
+ if (headers["mcp-method"] !== body.method) {
1211
+ return { code: HEADER_MISMATCH, message: "mcp-method header does not match the request body's method" };
1212
+ }
1213
+ const named = body.method === "tools/call" && isObject(body.params) ? body.params.name : void 0;
1214
+ if (named !== void 0 && named !== null && decodeHeaderValue(headers["mcp-name"]) !== named) {
1215
+ return { code: HEADER_MISMATCH, message: "mcp-name header does not match the request body's 'name' parameter" };
1216
+ }
1217
+ if (!MODERN_PROTOCOL_VERSIONS.includes(version)) return unsupported(String(version));
1218
+ return null;
1219
+ }
1220
+ function validParams(body) {
1221
+ const params = body.params ?? {};
1222
+ if (body.method === "tools/call") {
1223
+ return typeof params.name === "string" && (params.arguments === void 0 || isObject(params.arguments));
1224
+ }
1225
+ if (body.method === "tools/list") return params.cursor === void 0 || typeof params.cursor === "string";
1226
+ return true;
1227
+ }
1228
+ function unsupported(requested) {
1229
+ return { code: UNSUPPORTED_PROTOCOL_VERSION, message: "Unsupported protocol version", data: { supported: [...MODERN_PROTOCOL_VERSIONS], requested } };
1230
+ }
1231
+ function duplicatedHeader(rawHeaders) {
1232
+ const seen = /* @__PURE__ */ new Set();
1233
+ for (let i = 0; i < rawHeaders.length; i += 2) {
1234
+ const name = rawHeaders[i].toLowerCase();
1235
+ if (!ROUTING_HEADERS.has(name)) continue;
1236
+ if (seen.has(name)) return { code: HEADER_MISMATCH, message: `${name} header appears more than once` };
1237
+ seen.add(name);
1238
+ }
1239
+ return null;
1240
+ }
1241
+ function decodeHeaderValue(value) {
1242
+ if (typeof value !== "string") return void 0;
1243
+ const m = /^=\?base64\?(.*)\?=$/.exec(value);
1244
+ if (!m) return value;
1245
+ const bytes = Buffer.from(m[1], "base64");
1246
+ if (bytes.toString("base64") !== m[1]) return void 0;
1247
+ const text = new TextDecoder("utf-8", { fatal: true });
1248
+ try {
1249
+ return text.decode(bytes);
1250
+ } catch {
1251
+ return void 0;
1252
+ }
1253
+ }
1254
+ function envelopeVersion(body) {
1255
+ const meta = isObject(body) && isObject(body.params) ? body.params._meta : void 0;
1256
+ const version = isObject(meta) ? meta[META_PROTOCOL_VERSION] : void 0;
1257
+ return version === null ? void 0 : version;
1258
+ }
1259
+ function servedHosts(req, extra) {
1260
+ const port = req.socket.localPort;
1261
+ return /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`, ...extra]);
1262
+ }
1263
+ function foreignOrigin(origin, hosts, origins) {
1264
+ if (origin === void 0) return false;
1265
+ let url;
1266
+ try {
1267
+ url = new URL(origin);
1268
+ } catch {
1269
+ return true;
1270
+ }
1271
+ return !(hosts.has(url.host.toLowerCase()) || origins.includes(url.origin));
1272
+ }
1273
+ function acceptsJson(accept) {
1274
+ return (accept ?? "").split(",").some((part) => {
1275
+ const type = part.split(";")[0].trim().toLowerCase();
1276
+ return type === "application/json" || type === "application/*" || type === "*/*";
1277
+ });
1278
+ }
1279
+ function isJsonType(contentType) {
1280
+ return (contentType ?? "").toLowerCase().startsWith("application/json");
1281
+ }
1282
+ function answer(res, status, error, id) {
1283
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", ...id !== void 0 ? { id } : {}, error }));
1284
+ }
1285
+ function idOf(body) {
1286
+ if (!isObject(body)) return void 0;
1287
+ const id = body.id;
1288
+ return typeof id === "string" || typeof id === "number" && Number.isInteger(id) ? id : void 0;
1289
+ }
1290
+ var BodyTooLarge = class extends Error {
1291
+ };
1292
+ async function readJson(req, res, limit) {
1293
+ let text;
1294
+ try {
1295
+ text = await readBody(req, limit);
1296
+ } catch (err) {
1297
+ if (err instanceof BodyTooLarge) {
1298
+ answer(res, 413, { code: import_types2.ErrorCode.InvalidRequest, message: `Payload Too Large: Request body must not exceed ${limit} bytes` });
1299
+ } else {
1300
+ res.destroy();
1301
+ }
1302
+ return void 0;
1303
+ }
1304
+ try {
1305
+ return { value: JSON.parse(text) };
1306
+ } catch {
1307
+ answer(res, 400, { code: import_types2.ErrorCode.ParseError, message: "Parse error" });
1308
+ return void 0;
1309
+ }
1310
+ }
1311
+ async function readBody(req, limit) {
1312
+ if (Number(req.headers["content-length"]) > limit) throw new BodyTooLarge();
1313
+ const chunks = [];
1314
+ let size = 0;
1315
+ for await (const chunk of req) {
1316
+ size += chunk.length;
1317
+ if (size > limit) throw new BodyTooLarge();
1318
+ chunks.push(chunk);
1319
+ }
1320
+ return Buffer.concat(chunks).toString("utf8");
1321
+ }
1322
+ function isObject(value) {
1323
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1324
+ }
1325
+ var SingleExchangeTransport = class {
1326
+ constructor(onreply) {
1327
+ this.onreply = onreply;
1328
+ }
1329
+ onreply;
1330
+ onclose;
1331
+ onerror;
1332
+ onmessage;
1333
+ async start() {
1334
+ }
1335
+ async send(message) {
1336
+ if ("id" in message && ("result" in message || "error" in message)) this.onreply(message);
1337
+ }
1338
+ async close() {
1339
+ this.onclose?.();
1340
+ }
1341
+ };
1342
+
956
1343
  // src/index.ts
957
1344
  var META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
958
1345
  var LIST_TTL_MS = 36e5;
959
1346
  var ServerDiscoverRequestSchema = import_zod.z.object({
960
1347
  method: import_zod.z.literal("server/discover"),
961
- params: import_zod.z.optional(import_zod.z.object({ _meta: import_zod.z.optional(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())) }).loose())
1348
+ // `.passthrough()` keeps unknown keys under zod 3 and zod 4 alike, and the
1349
+ // package accepts both majors. The zod 4 name for the same thing does not
1350
+ // exist in zod 3, where it stopped the package at import.
1351
+ params: import_zod.z.optional(import_zod.z.object({ _meta: import_zod.z.optional(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())) }).passthrough())
962
1352
  });
1353
+ var INTERNAL_ERROR = -32603;
1354
+ var FAILED_INSIDE = { chap_error: INTERNAL_ERROR, message: "The CHAP call failed inside the server; its log has the detail." };
1355
+ function refusePerson(participant) {
1356
+ if (participant?.startsWith("human:")) {
1357
+ throw new Error(`participant ${participant} names a person. This server acts for an agent, and a person decides in a desk; give the agent's own URI, such as agent:assistant.`);
1358
+ }
1359
+ }
1360
+ function pinnedSchema(schema, participant) {
1361
+ if (!schema.properties?.from) return schema;
1362
+ return {
1363
+ ...schema,
1364
+ properties: {
1365
+ ...schema.properties,
1366
+ from: {
1367
+ type: "string",
1368
+ description: `This server acts as ${participant}: it fills this in, and refuses a call that names anyone else.`
1369
+ }
1370
+ },
1371
+ required: (schema.required ?? []).filter((key) => key !== "from")
1372
+ };
1373
+ }
963
1374
  function makeChapMcpServer(coord, options = {}) {
1375
+ refusePerson(options.participant);
964
1376
  const serverInfo = {
965
1377
  name: options.name ?? "chap",
966
- version: options.version ?? "0.3.3"
1378
+ version: options.version ?? "0.3.5"
967
1379
  };
968
1380
  const server = new import_server.Server(
969
1381
  serverInfo,
@@ -984,13 +1396,13 @@ function makeChapMcpServer(coord, options = {}) {
984
1396
  // not already see. The annotation carries a readable one.
985
1397
  title: ann?.title ?? name,
986
1398
  description: TOOL_DESCRIPTIONS[name] ?? `CHAP method ${methodForTool(name)}.`,
987
- inputSchema: SCHEMAS[name],
1399
+ inputSchema: options.participant === void 0 ? SCHEMAS[name] : pinnedSchema(SCHEMAS[name], options.participant),
988
1400
  // Behavioural hints. Without these a client has only the prose to go
989
1401
  // on when deciding whether a call is safe to make or to retry.
990
1402
  ...ann ? { annotations: ann } : {}
991
1403
  };
992
1404
  });
993
- server.setRequestHandler(import_types2.ListToolsRequestSchema, async (request) => {
1405
+ server.setRequestHandler(import_types3.ListToolsRequestSchema, async (request) => {
994
1406
  const era = classifyEnvelope(request.params);
995
1407
  if (era === "legacy") return { tools: enabledTools };
996
1408
  return {
@@ -1003,7 +1415,7 @@ function makeChapMcpServer(coord, options = {}) {
1003
1415
  });
1004
1416
  server.setRequestHandler(ServerDiscoverRequestSchema, async (request) => {
1005
1417
  if (classifyEnvelope(request.params) === "legacy") {
1006
- throw new ProtocolError(import_types2.ErrorCode.MethodNotFound, "Method not found", "server/discover");
1418
+ throw new ProtocolError(import_types3.ErrorCode.MethodNotFound, "Method not found", "server/discover");
1007
1419
  }
1008
1420
  return {
1009
1421
  resultType: "complete",
@@ -1015,19 +1427,34 @@ function makeChapMcpServer(coord, options = {}) {
1015
1427
  _meta: { [META_SERVER_INFO]: serverInfo }
1016
1428
  };
1017
1429
  });
1018
- server.setRequestHandler(import_types2.CallToolRequestSchema, async (request) => {
1430
+ server.setRequestHandler(import_types3.CallToolRequestSchema, async (request) => {
1019
1431
  const era = classifyEnvelope(request.params);
1020
1432
  const terminal = (result) => era === "modern" ? { resultType: "complete", ...result } : result;
1021
1433
  const toolName = request.params.name;
1022
1434
  const rawArgs = request.params.arguments ?? {};
1435
+ if (!Object.hasOwn(SCHEMAS, toolName)) {
1436
+ throw new ProtocolError(import_types3.ErrorCode.InvalidParams, `Unknown tool: ${toolName}`);
1437
+ }
1023
1438
  const method = methodForTool(toolName);
1024
- if (!method) {
1439
+ if (!filter(toolName)) {
1025
1440
  return terminal({
1026
1441
  isError: true,
1027
- content: [{ type: "text", text: `Unknown CHAP tool: ${toolName}` }]
1442
+ content: [{ type: "text", text: `CHAP tool not offered by this server: ${toolName}` }]
1028
1443
  });
1029
1444
  }
1030
- const args = coerceToolArgs(toolName, rawArgs);
1445
+ const args = { ...coerceToolArgs(toolName, rawArgs) };
1446
+ if (options.participant !== void 0) {
1447
+ if (args.from !== void 0 && args.from !== options.participant) {
1448
+ return terminal({
1449
+ isError: true,
1450
+ content: [{
1451
+ type: "text",
1452
+ text: `This server acts as ${options.participant}; a call cannot name another from (${JSON.stringify(args.from)}).`
1453
+ }]
1454
+ });
1455
+ }
1456
+ args.from = options.participant;
1457
+ }
1031
1458
  const envelope = {
1032
1459
  jsonrpc: "2.0",
1033
1460
  id: nextId(),
@@ -1036,20 +1463,25 @@ function makeChapMcpServer(coord, options = {}) {
1036
1463
  };
1037
1464
  let response;
1038
1465
  try {
1039
- response = coord.dispatch(envelope);
1466
+ response = await coord.dispatch(envelope);
1040
1467
  } catch (e) {
1041
- const msg = e instanceof Error ? e.message : String(e);
1468
+ if (e instanceof ChapEndpointError) {
1469
+ return terminal({ isError: true, content: [{ type: "text", text: e.message }] });
1470
+ }
1471
+ console.error("CHAP dispatch failed:", e);
1042
1472
  return terminal({
1043
1473
  isError: true,
1044
- content: [{ type: "text", text: `CHAP dispatch threw: ${msg}` }]
1474
+ content: [{ type: "text", text: FAILED_INSIDE.message }]
1045
1475
  });
1046
1476
  }
1047
1477
  if (response.error) {
1478
+ const internal = response.error.code === INTERNAL_ERROR;
1479
+ if (internal) console.error("CHAP call failed:", response.error);
1048
1480
  return terminal({
1049
1481
  isError: true,
1050
1482
  content: [{
1051
1483
  type: "text",
1052
- text: JSON.stringify({
1484
+ text: JSON.stringify(internal ? FAILED_INSIDE : {
1053
1485
  chap_error: response.error.code,
1054
1486
  message: response.error.message,
1055
1487
  ...response.error.data !== void 0 ? { data: response.error.data } : {}
@@ -1082,8 +1514,37 @@ function dispatchToolCall(coord, toolName, args, envelopeId = "mcp-call") {
1082
1514
  params: coerceToolArgs(toolName, args ?? {})
1083
1515
  });
1084
1516
  }
1517
+ function chapMcpHttpHandler(coord, options = {}) {
1518
+ const {
1519
+ maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
1520
+ timeoutMs = DEFAULT_TIMEOUT_MS,
1521
+ allowedHosts = [],
1522
+ allowedOrigins = [],
1523
+ ...serverOptions
1524
+ } = options;
1525
+ refusePerson(serverOptions.participant);
1526
+ return mcpHttpHandler({
1527
+ makeServer: () => makeChapMcpServer(coord, serverOptions),
1528
+ maxBodyBytes,
1529
+ timeoutMs,
1530
+ allowedHosts: allowedHosts.map((host) => host.toLowerCase()),
1531
+ allowedOrigins: allowedOrigins.map((origin) => {
1532
+ let parsed;
1533
+ try {
1534
+ parsed = new URL(origin).origin;
1535
+ } catch {
1536
+ parsed = void 0;
1537
+ }
1538
+ if (parsed === void 0 || parsed === "null") {
1539
+ throw new TypeError(`allowedOrigins takes origins such as http://localhost:6274; ${JSON.stringify(origin)} is not one`);
1540
+ }
1541
+ return parsed;
1542
+ })
1543
+ });
1544
+ }
1085
1545
  // Annotate the CommonJS export names for ESM import in node:
1086
1546
  0 && (module.exports = {
1547
+ AGENT_MCP_TOOLS,
1087
1548
  META_CLIENT_CAPABILITIES,
1088
1549
  META_CLIENT_INFO,
1089
1550
  META_PROTOCOL_VERSION,
@@ -1097,6 +1558,7 @@ function dispatchToolCall(coord, toolName, args, envelopeId = "mcp-call") {
1097
1558
  TOOL_DESCRIPTIONS,
1098
1559
  TOOL_NAMES,
1099
1560
  UNSUPPORTED_PROTOCOL_VERSION,
1561
+ chapMcpHttpHandler,
1100
1562
  classifyEnvelope,
1101
1563
  coerceToolArgs,
1102
1564
  dispatchToolCall,