@brightbeamai/chap-coordinator-mcp 0.3.4 → 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.js CHANGED
@@ -3,7 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import {
4
4
  CallToolRequestSchema,
5
5
  ListToolsRequestSchema,
6
- ErrorCode as ErrorCode2
6
+ ErrorCode as ErrorCode3
7
7
  } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { z } from "zod";
9
9
 
@@ -72,6 +72,18 @@ var SCHEMAS = {
72
72
  enum: ["shadow", "trial", "production"],
73
73
  default: "production",
74
74
  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."
75
+ },
76
+ join_policy: {
77
+ type: "string",
78
+ enum: ["open", "invite"],
79
+ default: "open",
80
+ 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."
81
+ },
82
+ review_rule: {
83
+ type: "string",
84
+ pattern: "^(any_one_approves|all_approve|quorum:[1-9][0-9]*)$",
85
+ default: "any_one_approves",
86
+ 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."
75
87
  }
76
88
  },
77
89
  additionalProperties: false
@@ -93,6 +105,26 @@ var SCHEMAS = {
93
105
  },
94
106
  required: ["workspace", "profiles"]
95
107
  },
108
+ "chap.workspace.invite": {
109
+ type: "object",
110
+ properties: {
111
+ workspace: WORKSPACE_ID,
112
+ from: { ...PARTICIPANT_URI, description: "The admin who invites. Any other sender is refused with -32011." },
113
+ uri: { ...PARTICIPANT_URI, description: "The participant invited. Its scheme must name type, as at a join." },
114
+ type: {
115
+ type: "string",
116
+ enum: ["human", "agent", "service"],
117
+ 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."
118
+ },
119
+ 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." },
120
+ jwks: {
121
+ type: "object",
122
+ 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."
123
+ },
124
+ 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." }
125
+ },
126
+ required: ["workspace", "from", "uri", "type"]
127
+ },
96
128
  "chap.participant.join": {
97
129
  type: "object",
98
130
  properties: {
@@ -100,10 +132,10 @@ var SCHEMAS = {
100
132
  from: PARTICIPANT_URI,
101
133
  type: {
102
134
  type: "string",
103
- enum: ["human", "agent", "service", "group", "workspace"],
104
- 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."
135
+ enum: ["human", "agent", "service"],
136
+ 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."
105
137
  },
106
- 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." },
138
+ 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." },
107
139
  display_name: { type: "string", description: "Human-readable name for interfaces. Not used in authorisation." }
108
140
  },
109
141
  required: ["workspace", "from", "type"]
@@ -219,6 +251,11 @@ var SCHEMAS = {
219
251
  },
220
252
  required: ["workspace", "from", "task_id", "to", "artefact"]
221
253
  },
254
+ "chap.review.acknowledge": {
255
+ type: "object",
256
+ properties: { workspace: WORKSPACE_ID, from: PARTICIPANT_URI, task_id: TASK_ID },
257
+ required: ["workspace", "from", "task_id"]
258
+ },
222
259
  "chap.decide.approve": {
223
260
  type: "object",
224
261
  properties: {
@@ -723,19 +760,21 @@ var TOOL_DESCRIPTIONS = {
723
760
  "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.",
724
761
  "chap.workspace.describe": "Report the current state of a workspace: its members, enabled profiles, audit length, and task and override counts.",
725
762
  "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.",
763
+ "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).",
726
764
  "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.",
727
765
  "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.",
728
766
  "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.",
729
- "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.",
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. A task whose review no decision has closed, or a successor that carries one on, is not declined (-32602).",
730
768
  "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.",
731
769
  "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.",
732
770
  // review/1.0
733
- "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.",
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, 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.",
772
+ "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.",
734
773
  "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.",
735
774
  "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.",
736
775
  "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.",
737
776
  "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.",
738
- "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.",
777
+ "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.",
739
778
  // whisper/1.0
740
779
  "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.",
741
780
  "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.",
@@ -751,10 +790,10 @@ var TOOL_DESCRIPTIONS = {
751
790
  // control/1.0
752
791
  "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.",
753
792
  "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.",
754
- "chap.control.cancel": "Cancel a task. Cancelled is terminal, and a task that has already settled cannot be cancelled.",
793
+ "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).",
755
794
  "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.",
756
795
  "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.",
757
- "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.",
796
+ "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).",
758
797
  "chap.control.set_mode_ceiling": "Set the highest operating mode tasks in this workspace may request. A task above the ceiling is refused.",
759
798
  // routing/1.0
760
799
  "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.",
@@ -805,8 +844,10 @@ var IDEMPOTENT = /* @__PURE__ */ new Set([
805
844
  // pausing an already paused target changes nothing
806
845
  "chap.control.set_mode_ceiling",
807
846
  // setting the ceiling it already has changes nothing
808
- "chap.deliberate.close"
847
+ "chap.deliberate.close",
809
848
  // returns the same outcome, recomputing nothing
849
+ "chap.review.acknowledge"
850
+ // a second acknowledgement in the round is refused and records nothing
810
851
  ]);
811
852
  var OPEN_WORLD = /* @__PURE__ */ new Set([
812
853
  "chap.audit.submit_to_scitt",
@@ -816,6 +857,7 @@ var TITLES = {
816
857
  "chap.workspace.create": "Create workspace",
817
858
  "chap.workspace.describe": "Describe workspace",
818
859
  "chap.workspace.set_profiles": "Replace workspace profiles",
860
+ "chap.workspace.invite": "Invite participant",
819
861
  "chap.participant.join": "Add participant",
820
862
  "chap.participant.leave": "Remove participant",
821
863
  "chap.task.create": "Create task",
@@ -823,6 +865,7 @@ var TITLES = {
823
865
  "chap.task.complete": "Submit task output",
824
866
  "chap.audit.read": "Read audit log",
825
867
  "chap.review.request": "Request review",
868
+ "chap.review.acknowledge": "Acknowledge review",
826
869
  "chap.decide.approve": "Approve",
827
870
  "chap.decide.reject": "Reject",
828
871
  "chap.decide.override": "Override with a correction",
@@ -863,6 +906,19 @@ var TOOL_ANNOTATIONS = Object.fromEntries(
863
906
  openWorldHint: OPEN_WORLD.has(name)
864
907
  }])
865
908
  );
909
+ var AGENT_MCP_TOOLS = /* @__PURE__ */ new Set([
910
+ "chap.task.create",
911
+ "chap.task.update",
912
+ "chap.task.complete",
913
+ "chap.whisper.ask",
914
+ "chap.decide.approve",
915
+ "chap.decide.reject",
916
+ "chap.decide.override",
917
+ "chap.review.acknowledge",
918
+ "chap.workspace.describe",
919
+ "chap.audit.read",
920
+ "chap.audit.verify_chain"
921
+ ]);
866
922
 
867
923
  // src/envelope.ts
868
924
  import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
@@ -915,6 +971,340 @@ function readMeta(params) {
915
971
  return meta;
916
972
  }
917
973
 
974
+ // src/remote.ts
975
+ var ChapEndpointError = class extends Error {
976
+ constructor(message) {
977
+ super(message);
978
+ this.name = "ChapEndpointError";
979
+ }
980
+ };
981
+
982
+ // src/http.ts
983
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
984
+ import {
985
+ ErrorCode as ErrorCode2,
986
+ SUPPORTED_PROTOCOL_VERSIONS as SDK_PROTOCOL_VERSIONS,
987
+ isJSONRPCNotification,
988
+ isJSONRPCRequest
989
+ } from "@modelcontextprotocol/sdk/types.js";
990
+ var HEADER_MISMATCH = -32020;
991
+ var DEFAULT_MAX_BODY_BYTES = 1048576 + 65536;
992
+ var DEFAULT_TIMEOUT_MS = 45e3;
993
+ var HANDSHAKE_VERSIONS = SDK_PROTOCOL_VERSIONS.filter(
994
+ (v) => !MODERN_PROTOCOL_VERSIONS.includes(v)
995
+ );
996
+ var MODERN_METHODS = /* @__PURE__ */ new Set(["server/discover", "tools/list", "tools/call"]);
997
+ var ROUTING_HEADERS = /* @__PURE__ */ new Set(["mcp-protocol-version", "mcp-method", "mcp-name"]);
998
+ var STATUS_FOR_CODE = /* @__PURE__ */ new Map([
999
+ [ErrorCode2.ParseError, 400],
1000
+ [ErrorCode2.InvalidRequest, 400],
1001
+ [ErrorCode2.InvalidParams, 400],
1002
+ [HEADER_MISMATCH, 400],
1003
+ [UNSUPPORTED_PROTOCOL_VERSION, 400],
1004
+ [ErrorCode2.MethodNotFound, 404]
1005
+ ]);
1006
+ var NOT_ONE_MESSAGE = "Body must be a single JSON-RPC request or notification object";
1007
+ var INTERNAL = { code: ErrorCode2.InternalError, message: "Internal error" };
1008
+ function mcpHttpHandler(settings) {
1009
+ return async function handle(req, res, parsedBody) {
1010
+ try {
1011
+ const hosts = servedHosts(req, settings.allowedHosts);
1012
+ if (!hosts.has((req.headers.host ?? "").toLowerCase())) {
1013
+ answer(res, 421, { code: ErrorCode2.InvalidRequest, message: "Unknown host: add it to allowedHosts to serve under that name" });
1014
+ return;
1015
+ }
1016
+ if (foreignOrigin(req.headers.origin, hosts, settings.allowedOrigins)) {
1017
+ answer(res, 403, { code: ErrorCode2.InvalidRequest, message: "Cross-origin requests are refused: add the origin to allowedOrigins to serve it" });
1018
+ return;
1019
+ }
1020
+ if (req.method !== "POST") {
1021
+ res.writeHead(405, { allow: "POST" }).end();
1022
+ return;
1023
+ }
1024
+ const declared = req.headers["mcp-protocol-version"];
1025
+ if (declared === void 0 || typeof declared === "string" && HANDSHAKE_VERSIONS.includes(declared)) {
1026
+ await handshake(settings, req, res, parsedBody);
1027
+ return;
1028
+ }
1029
+ await singleExchange(settings, req, res, parsedBody);
1030
+ } catch (err) {
1031
+ console.error("MCP request failed:", err);
1032
+ if (!res.headersSent) answer(res, 500, INTERNAL);
1033
+ else res.destroy();
1034
+ }
1035
+ };
1036
+ }
1037
+ async function handshake(settings, req, res, parsedBody) {
1038
+ let body = parsedBody;
1039
+ if (body === void 0 && isJsonType(req.headers["content-type"])) {
1040
+ const read = await readJson(req, res, settings.maxBodyBytes);
1041
+ if (!read) return;
1042
+ body = read.value;
1043
+ }
1044
+ if (Array.isArray(body)) {
1045
+ answer(res, 400, { code: ErrorCode2.InvalidRequest, message: "A batch is not accepted: send each JSON-RPC message in a POST of its own" });
1046
+ return;
1047
+ }
1048
+ const header = req.headers["mcp-protocol-version"];
1049
+ const version = envelopeVersion(body);
1050
+ if (version !== void 0 && version !== header) {
1051
+ answer(res, 400, {
1052
+ code: HEADER_MISMATCH,
1053
+ 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"
1054
+ }, idOf(body));
1055
+ return;
1056
+ }
1057
+ const id = isJSONRPCRequest(body) ? body.id : void 0;
1058
+ const server = settings.makeServer();
1059
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, maxRequestBodySize: settings.maxBodyBytes });
1060
+ const timer = setTimeout(() => {
1061
+ console.error(`MCP request ${JSON.stringify(id ?? null)} had no answer within ${settings.timeoutMs} ms`);
1062
+ if (!res.headersSent) answer(res, 500, INTERNAL, id);
1063
+ else if (id !== void 0) transport.send({ jsonrpc: "2.0", id, error: INTERNAL }).catch(() => res.destroy());
1064
+ else res.destroy();
1065
+ }, settings.timeoutMs);
1066
+ res.on("close", () => {
1067
+ clearTimeout(timer);
1068
+ void transport.close();
1069
+ void server.close();
1070
+ });
1071
+ await server.connect(transport);
1072
+ await transport.handleRequest(req, res, body);
1073
+ }
1074
+ async function singleExchange(settings, req, res, parsedBody) {
1075
+ if (!acceptsJson(req.headers.accept)) {
1076
+ res.writeHead(406).end();
1077
+ return;
1078
+ }
1079
+ if (!isJsonType(req.headers["content-type"])) {
1080
+ answer(res, 415, { code: ErrorCode2.InvalidRequest, message: "Unsupported Media Type: Content-Type must be application/json" });
1081
+ return;
1082
+ }
1083
+ let body = parsedBody;
1084
+ if (body === void 0) {
1085
+ const read = await readJson(req, res, settings.maxBodyBytes);
1086
+ if (!read) return;
1087
+ body = read.value;
1088
+ }
1089
+ if (isObject(body) && !("id" in body)) {
1090
+ if (!isJSONRPCNotification(body)) {
1091
+ answer(res, 400, { code: ErrorCode2.InvalidRequest, message: NOT_ONE_MESSAGE });
1092
+ return;
1093
+ }
1094
+ const version = String(req.headers["mcp-protocol-version"]);
1095
+ if (!MODERN_PROTOCOL_VERSIONS.includes(version)) {
1096
+ answer(res, 400, unsupported(version));
1097
+ return;
1098
+ }
1099
+ res.writeHead(202).end();
1100
+ return;
1101
+ }
1102
+ if (!isJSONRPCRequest(body)) {
1103
+ answer(res, 400, { code: ErrorCode2.InvalidRequest, message: NOT_ONE_MESSAGE }, idOf(body));
1104
+ return;
1105
+ }
1106
+ const refusal = duplicatedHeader(req.rawHeaders) ?? classify(body, req.headers);
1107
+ if (refusal) {
1108
+ answer(res, STATUS_FOR_CODE.get(refusal.code) ?? 200, refusal, body.id);
1109
+ return;
1110
+ }
1111
+ if (!MODERN_METHODS.has(body.method)) {
1112
+ answer(res, 404, { code: ErrorCode2.MethodNotFound, message: "Method not found", data: body.method }, body.id);
1113
+ return;
1114
+ }
1115
+ if (!validParams(body)) {
1116
+ answer(res, 400, { code: ErrorCode2.InvalidParams, message: "Invalid request parameters" }, body.id);
1117
+ return;
1118
+ }
1119
+ const outcome = await exchange(settings, res, body);
1120
+ switch (outcome.kind) {
1121
+ case "reply": {
1122
+ const error = outcome.message.error;
1123
+ const status = error ? STATUS_FOR_CODE.get(error.code) ?? 200 : 200;
1124
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(outcome.message));
1125
+ return;
1126
+ }
1127
+ case "gone":
1128
+ return;
1129
+ case "timeout":
1130
+ console.error(`MCP request ${JSON.stringify(body.id)} had no answer within ${settings.timeoutMs} ms`);
1131
+ answer(res, 500, INTERNAL, body.id);
1132
+ return;
1133
+ case "error":
1134
+ console.error("MCP request failed:", outcome.error);
1135
+ answer(res, 500, INTERNAL, body.id);
1136
+ }
1137
+ }
1138
+ async function exchange(settings, res, request) {
1139
+ let settle;
1140
+ const outcome = new Promise((resolve) => {
1141
+ settle = resolve;
1142
+ });
1143
+ const timer = setTimeout(() => settle({ kind: "timeout" }), settings.timeoutMs);
1144
+ const gone = () => settle({ kind: "gone" });
1145
+ res.once("close", gone);
1146
+ const server = settings.makeServer();
1147
+ server.onerror = (error) => settle({ kind: "error", error });
1148
+ const transport = new SingleExchangeTransport((message) => settle({ kind: "reply", message }));
1149
+ try {
1150
+ await server.connect(transport);
1151
+ transport.onmessage?.(request);
1152
+ return await outcome;
1153
+ } finally {
1154
+ clearTimeout(timer);
1155
+ res.off("close", gone);
1156
+ await server.close();
1157
+ }
1158
+ }
1159
+ function classify(body, headers) {
1160
+ const meta = isObject(body.params) ? body.params._meta : void 0;
1161
+ if (!isObject(meta)) {
1162
+ return {
1163
+ code: ErrorCode2.InvalidParams,
1164
+ message: `params._meta must be an object carrying the required '${META_PROTOCOL_VERSION}' and '${META_CLIENT_CAPABILITIES}' envelope keys`
1165
+ };
1166
+ }
1167
+ const missing = [META_PROTOCOL_VERSION, META_CLIENT_CAPABILITIES].filter((key) => !(key in meta));
1168
+ if (missing.length) {
1169
+ return { code: ErrorCode2.InvalidParams, message: `params._meta is missing the required envelope key(s): ${missing.join(", ")}` };
1170
+ }
1171
+ const version = meta[META_PROTOCOL_VERSION];
1172
+ if (headers["mcp-protocol-version"] !== version) {
1173
+ return { code: HEADER_MISMATCH, message: "mcp-protocol-version header does not match the request envelope's protocol version" };
1174
+ }
1175
+ if (headers["mcp-method"] !== body.method) {
1176
+ return { code: HEADER_MISMATCH, message: "mcp-method header does not match the request body's method" };
1177
+ }
1178
+ const named = body.method === "tools/call" && isObject(body.params) ? body.params.name : void 0;
1179
+ if (named !== void 0 && named !== null && decodeHeaderValue(headers["mcp-name"]) !== named) {
1180
+ return { code: HEADER_MISMATCH, message: "mcp-name header does not match the request body's 'name' parameter" };
1181
+ }
1182
+ if (!MODERN_PROTOCOL_VERSIONS.includes(version)) return unsupported(String(version));
1183
+ return null;
1184
+ }
1185
+ function validParams(body) {
1186
+ const params = body.params ?? {};
1187
+ if (body.method === "tools/call") {
1188
+ return typeof params.name === "string" && (params.arguments === void 0 || isObject(params.arguments));
1189
+ }
1190
+ if (body.method === "tools/list") return params.cursor === void 0 || typeof params.cursor === "string";
1191
+ return true;
1192
+ }
1193
+ function unsupported(requested) {
1194
+ return { code: UNSUPPORTED_PROTOCOL_VERSION, message: "Unsupported protocol version", data: { supported: [...MODERN_PROTOCOL_VERSIONS], requested } };
1195
+ }
1196
+ function duplicatedHeader(rawHeaders) {
1197
+ const seen = /* @__PURE__ */ new Set();
1198
+ for (let i = 0; i < rawHeaders.length; i += 2) {
1199
+ const name = rawHeaders[i].toLowerCase();
1200
+ if (!ROUTING_HEADERS.has(name)) continue;
1201
+ if (seen.has(name)) return { code: HEADER_MISMATCH, message: `${name} header appears more than once` };
1202
+ seen.add(name);
1203
+ }
1204
+ return null;
1205
+ }
1206
+ function decodeHeaderValue(value) {
1207
+ if (typeof value !== "string") return void 0;
1208
+ const m = /^=\?base64\?(.*)\?=$/.exec(value);
1209
+ if (!m) return value;
1210
+ const bytes = Buffer.from(m[1], "base64");
1211
+ if (bytes.toString("base64") !== m[1]) return void 0;
1212
+ const text = new TextDecoder("utf-8", { fatal: true });
1213
+ try {
1214
+ return text.decode(bytes);
1215
+ } catch {
1216
+ return void 0;
1217
+ }
1218
+ }
1219
+ function envelopeVersion(body) {
1220
+ const meta = isObject(body) && isObject(body.params) ? body.params._meta : void 0;
1221
+ const version = isObject(meta) ? meta[META_PROTOCOL_VERSION] : void 0;
1222
+ return version === null ? void 0 : version;
1223
+ }
1224
+ function servedHosts(req, extra) {
1225
+ const port = req.socket.localPort;
1226
+ return /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`, ...extra]);
1227
+ }
1228
+ function foreignOrigin(origin, hosts, origins) {
1229
+ if (origin === void 0) return false;
1230
+ let url;
1231
+ try {
1232
+ url = new URL(origin);
1233
+ } catch {
1234
+ return true;
1235
+ }
1236
+ return !(hosts.has(url.host.toLowerCase()) || origins.includes(url.origin));
1237
+ }
1238
+ function acceptsJson(accept) {
1239
+ return (accept ?? "").split(",").some((part) => {
1240
+ const type = part.split(";")[0].trim().toLowerCase();
1241
+ return type === "application/json" || type === "application/*" || type === "*/*";
1242
+ });
1243
+ }
1244
+ function isJsonType(contentType) {
1245
+ return (contentType ?? "").toLowerCase().startsWith("application/json");
1246
+ }
1247
+ function answer(res, status, error, id) {
1248
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", ...id !== void 0 ? { id } : {}, error }));
1249
+ }
1250
+ function idOf(body) {
1251
+ if (!isObject(body)) return void 0;
1252
+ const id = body.id;
1253
+ return typeof id === "string" || typeof id === "number" && Number.isInteger(id) ? id : void 0;
1254
+ }
1255
+ var BodyTooLarge = class extends Error {
1256
+ };
1257
+ async function readJson(req, res, limit) {
1258
+ let text;
1259
+ try {
1260
+ text = await readBody(req, limit);
1261
+ } catch (err) {
1262
+ if (err instanceof BodyTooLarge) {
1263
+ answer(res, 413, { code: ErrorCode2.InvalidRequest, message: `Payload Too Large: Request body must not exceed ${limit} bytes` });
1264
+ } else {
1265
+ res.destroy();
1266
+ }
1267
+ return void 0;
1268
+ }
1269
+ try {
1270
+ return { value: JSON.parse(text) };
1271
+ } catch {
1272
+ answer(res, 400, { code: ErrorCode2.ParseError, message: "Parse error" });
1273
+ return void 0;
1274
+ }
1275
+ }
1276
+ async function readBody(req, limit) {
1277
+ if (Number(req.headers["content-length"]) > limit) throw new BodyTooLarge();
1278
+ const chunks = [];
1279
+ let size = 0;
1280
+ for await (const chunk of req) {
1281
+ size += chunk.length;
1282
+ if (size > limit) throw new BodyTooLarge();
1283
+ chunks.push(chunk);
1284
+ }
1285
+ return Buffer.concat(chunks).toString("utf8");
1286
+ }
1287
+ function isObject(value) {
1288
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1289
+ }
1290
+ var SingleExchangeTransport = class {
1291
+ constructor(onreply) {
1292
+ this.onreply = onreply;
1293
+ }
1294
+ onreply;
1295
+ onclose;
1296
+ onerror;
1297
+ onmessage;
1298
+ async start() {
1299
+ }
1300
+ async send(message) {
1301
+ if ("id" in message && ("result" in message || "error" in message)) this.onreply(message);
1302
+ }
1303
+ async close() {
1304
+ this.onclose?.();
1305
+ }
1306
+ };
1307
+
918
1308
  // src/index.ts
919
1309
  var META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
920
1310
  var LIST_TTL_MS = 36e5;
@@ -925,10 +1315,32 @@ var ServerDiscoverRequestSchema = z.object({
925
1315
  // exist in zod 3, where it stopped the package at import.
926
1316
  params: z.optional(z.object({ _meta: z.optional(z.record(z.string(), z.unknown())) }).passthrough())
927
1317
  });
1318
+ var INTERNAL_ERROR = -32603;
1319
+ var FAILED_INSIDE = { chap_error: INTERNAL_ERROR, message: "The CHAP call failed inside the server; its log has the detail." };
1320
+ function refusePerson(participant) {
1321
+ if (participant?.startsWith("human:")) {
1322
+ 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.`);
1323
+ }
1324
+ }
1325
+ function pinnedSchema(schema, participant) {
1326
+ if (!schema.properties?.from) return schema;
1327
+ return {
1328
+ ...schema,
1329
+ properties: {
1330
+ ...schema.properties,
1331
+ from: {
1332
+ type: "string",
1333
+ description: `This server acts as ${participant}: it fills this in, and refuses a call that names anyone else.`
1334
+ }
1335
+ },
1336
+ required: (schema.required ?? []).filter((key) => key !== "from")
1337
+ };
1338
+ }
928
1339
  function makeChapMcpServer(coord, options = {}) {
1340
+ refusePerson(options.participant);
929
1341
  const serverInfo = {
930
1342
  name: options.name ?? "chap",
931
- version: options.version ?? "0.3.4"
1343
+ version: options.version ?? "0.3.5"
932
1344
  };
933
1345
  const server = new Server(
934
1346
  serverInfo,
@@ -949,7 +1361,7 @@ function makeChapMcpServer(coord, options = {}) {
949
1361
  // not already see. The annotation carries a readable one.
950
1362
  title: ann?.title ?? name,
951
1363
  description: TOOL_DESCRIPTIONS[name] ?? `CHAP method ${methodForTool(name)}.`,
952
- inputSchema: SCHEMAS[name],
1364
+ inputSchema: options.participant === void 0 ? SCHEMAS[name] : pinnedSchema(SCHEMAS[name], options.participant),
953
1365
  // Behavioural hints. Without these a client has only the prose to go
954
1366
  // on when deciding whether a call is safe to make or to retry.
955
1367
  ...ann ? { annotations: ann } : {}
@@ -968,7 +1380,7 @@ function makeChapMcpServer(coord, options = {}) {
968
1380
  });
969
1381
  server.setRequestHandler(ServerDiscoverRequestSchema, async (request) => {
970
1382
  if (classifyEnvelope(request.params) === "legacy") {
971
- throw new ProtocolError(ErrorCode2.MethodNotFound, "Method not found", "server/discover");
1383
+ throw new ProtocolError(ErrorCode3.MethodNotFound, "Method not found", "server/discover");
972
1384
  }
973
1385
  return {
974
1386
  resultType: "complete",
@@ -985,13 +1397,10 @@ function makeChapMcpServer(coord, options = {}) {
985
1397
  const terminal = (result) => era === "modern" ? { resultType: "complete", ...result } : result;
986
1398
  const toolName = request.params.name;
987
1399
  const rawArgs = request.params.arguments ?? {};
988
- const method = methodForTool(toolName);
989
- if (!method) {
990
- return terminal({
991
- isError: true,
992
- content: [{ type: "text", text: `Unknown CHAP tool: ${toolName}` }]
993
- });
1400
+ if (!Object.hasOwn(SCHEMAS, toolName)) {
1401
+ throw new ProtocolError(ErrorCode3.InvalidParams, `Unknown tool: ${toolName}`);
994
1402
  }
1403
+ const method = methodForTool(toolName);
995
1404
  if (!filter(toolName)) {
996
1405
  return terminal({
997
1406
  isError: true,
@@ -1019,20 +1428,25 @@ function makeChapMcpServer(coord, options = {}) {
1019
1428
  };
1020
1429
  let response;
1021
1430
  try {
1022
- response = coord.dispatch(envelope);
1431
+ response = await coord.dispatch(envelope);
1023
1432
  } catch (e) {
1024
- const msg = e instanceof Error ? e.message : String(e);
1433
+ if (e instanceof ChapEndpointError) {
1434
+ return terminal({ isError: true, content: [{ type: "text", text: e.message }] });
1435
+ }
1436
+ console.error("CHAP dispatch failed:", e);
1025
1437
  return terminal({
1026
1438
  isError: true,
1027
- content: [{ type: "text", text: `CHAP dispatch threw: ${msg}` }]
1439
+ content: [{ type: "text", text: FAILED_INSIDE.message }]
1028
1440
  });
1029
1441
  }
1030
1442
  if (response.error) {
1443
+ const internal = response.error.code === INTERNAL_ERROR;
1444
+ if (internal) console.error("CHAP call failed:", response.error);
1031
1445
  return terminal({
1032
1446
  isError: true,
1033
1447
  content: [{
1034
1448
  type: "text",
1035
- text: JSON.stringify({
1449
+ text: JSON.stringify(internal ? FAILED_INSIDE : {
1036
1450
  chap_error: response.error.code,
1037
1451
  message: response.error.message,
1038
1452
  ...response.error.data !== void 0 ? { data: response.error.data } : {}
@@ -1065,7 +1479,36 @@ function dispatchToolCall(coord, toolName, args, envelopeId = "mcp-call") {
1065
1479
  params: coerceToolArgs(toolName, args ?? {})
1066
1480
  });
1067
1481
  }
1482
+ function chapMcpHttpHandler(coord, options = {}) {
1483
+ const {
1484
+ maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
1485
+ timeoutMs = DEFAULT_TIMEOUT_MS,
1486
+ allowedHosts = [],
1487
+ allowedOrigins = [],
1488
+ ...serverOptions
1489
+ } = options;
1490
+ refusePerson(serverOptions.participant);
1491
+ return mcpHttpHandler({
1492
+ makeServer: () => makeChapMcpServer(coord, serverOptions),
1493
+ maxBodyBytes,
1494
+ timeoutMs,
1495
+ allowedHosts: allowedHosts.map((host) => host.toLowerCase()),
1496
+ allowedOrigins: allowedOrigins.map((origin) => {
1497
+ let parsed;
1498
+ try {
1499
+ parsed = new URL(origin).origin;
1500
+ } catch {
1501
+ parsed = void 0;
1502
+ }
1503
+ if (parsed === void 0 || parsed === "null") {
1504
+ throw new TypeError(`allowedOrigins takes origins such as http://localhost:6274; ${JSON.stringify(origin)} is not one`);
1505
+ }
1506
+ return parsed;
1507
+ })
1508
+ });
1509
+ }
1068
1510
  export {
1511
+ AGENT_MCP_TOOLS,
1069
1512
  META_CLIENT_CAPABILITIES,
1070
1513
  META_CLIENT_INFO,
1071
1514
  META_PROTOCOL_VERSION,
@@ -1079,6 +1522,7 @@ export {
1079
1522
  TOOL_DESCRIPTIONS,
1080
1523
  TOOL_NAMES,
1081
1524
  UNSUPPORTED_PROTOCOL_VERSION,
1525
+ chapMcpHttpHandler,
1082
1526
  classifyEnvelope,
1083
1527
  coerceToolArgs,
1084
1528
  dispatchToolCall,