@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/cli.cjs CHANGED
@@ -24,12 +24,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_node_fs = require("fs");
28
+ var import_node_os = require("os");
29
+ var import_node_path = require("path");
27
30
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28
31
  var import_chap_coordinator = require("@brightbeamai/chap-coordinator");
29
32
 
30
33
  // src/index.ts
31
34
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
32
- var import_types2 = require("@modelcontextprotocol/sdk/types.js");
35
+ var import_types3 = require("@modelcontextprotocol/sdk/types.js");
33
36
  var import_zod = require("zod");
34
37
 
35
38
  // src/schemas.ts
@@ -97,6 +100,18 @@ var SCHEMAS = {
97
100
  enum: ["shadow", "trial", "production"],
98
101
  default: "production",
99
102
  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."
103
+ },
104
+ join_policy: {
105
+ type: "string",
106
+ enum: ["open", "invite"],
107
+ default: "open",
108
+ 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."
109
+ },
110
+ review_rule: {
111
+ type: "string",
112
+ pattern: "^(any_one_approves|all_approve|quorum:[1-9][0-9]*)$",
113
+ default: "any_one_approves",
114
+ 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."
100
115
  }
101
116
  },
102
117
  additionalProperties: false
@@ -118,6 +133,26 @@ var SCHEMAS = {
118
133
  },
119
134
  required: ["workspace", "profiles"]
120
135
  },
136
+ "chap.workspace.invite": {
137
+ type: "object",
138
+ properties: {
139
+ workspace: WORKSPACE_ID,
140
+ from: { ...PARTICIPANT_URI, description: "The admin who invites. Any other sender is refused with -32011." },
141
+ uri: { ...PARTICIPANT_URI, description: "The participant invited. Its scheme must name type, as at a join." },
142
+ type: {
143
+ type: "string",
144
+ enum: ["human", "agent", "service"],
145
+ 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."
146
+ },
147
+ 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." },
148
+ jwks: {
149
+ type: "object",
150
+ 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."
151
+ },
152
+ 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." }
153
+ },
154
+ required: ["workspace", "from", "uri", "type"]
155
+ },
121
156
  "chap.participant.join": {
122
157
  type: "object",
123
158
  properties: {
@@ -125,10 +160,10 @@ var SCHEMAS = {
125
160
  from: PARTICIPANT_URI,
126
161
  type: {
127
162
  type: "string",
128
- enum: ["human", "agent", "service", "group", "workspace"],
129
- 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."
163
+ enum: ["human", "agent", "service"],
164
+ 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."
130
165
  },
131
- 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." },
166
+ 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." },
132
167
  display_name: { type: "string", description: "Human-readable name for interfaces. Not used in authorisation." }
133
168
  },
134
169
  required: ["workspace", "from", "type"]
@@ -244,6 +279,11 @@ var SCHEMAS = {
244
279
  },
245
280
  required: ["workspace", "from", "task_id", "to", "artefact"]
246
281
  },
282
+ "chap.review.acknowledge": {
283
+ type: "object",
284
+ properties: { workspace: WORKSPACE_ID, from: PARTICIPANT_URI, task_id: TASK_ID },
285
+ required: ["workspace", "from", "task_id"]
286
+ },
247
287
  "chap.decide.approve": {
248
288
  type: "object",
249
289
  properties: {
@@ -748,19 +788,21 @@ var TOOL_DESCRIPTIONS = {
748
788
  "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.",
749
789
  "chap.workspace.describe": "Report the current state of a workspace: its members, enabled profiles, audit length, and task and override counts.",
750
790
  "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.",
791
+ "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).",
751
792
  "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.",
752
793
  "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.",
753
794
  "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.",
754
- "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.",
795
+ "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).",
755
796
  "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.",
756
797
  "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.",
757
798
  // review/1.0
758
- "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.",
799
+ "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.",
800
+ "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.",
759
801
  "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.",
760
802
  "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.",
761
803
  "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.",
762
804
  "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.",
763
- "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.",
805
+ "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.",
764
806
  // whisper/1.0
765
807
  "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.",
766
808
  "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.",
@@ -776,10 +818,10 @@ var TOOL_DESCRIPTIONS = {
776
818
  // control/1.0
777
819
  "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.",
778
820
  "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.",
779
- "chap.control.cancel": "Cancel a task. Cancelled is terminal, and a task that has already settled cannot be cancelled.",
821
+ "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).",
780
822
  "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.",
781
823
  "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.",
782
- "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.",
824
+ "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).",
783
825
  "chap.control.set_mode_ceiling": "Set the highest operating mode tasks in this workspace may request. A task above the ceiling is refused.",
784
826
  // routing/1.0
785
827
  "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.",
@@ -830,8 +872,10 @@ var IDEMPOTENT = /* @__PURE__ */ new Set([
830
872
  // pausing an already paused target changes nothing
831
873
  "chap.control.set_mode_ceiling",
832
874
  // setting the ceiling it already has changes nothing
833
- "chap.deliberate.close"
875
+ "chap.deliberate.close",
834
876
  // returns the same outcome, recomputing nothing
877
+ "chap.review.acknowledge"
878
+ // a second acknowledgement in the round is refused and records nothing
835
879
  ]);
836
880
  var OPEN_WORLD = /* @__PURE__ */ new Set([
837
881
  "chap.audit.submit_to_scitt",
@@ -841,6 +885,7 @@ var TITLES = {
841
885
  "chap.workspace.create": "Create workspace",
842
886
  "chap.workspace.describe": "Describe workspace",
843
887
  "chap.workspace.set_profiles": "Replace workspace profiles",
888
+ "chap.workspace.invite": "Invite participant",
844
889
  "chap.participant.join": "Add participant",
845
890
  "chap.participant.leave": "Remove participant",
846
891
  "chap.task.create": "Create task",
@@ -848,6 +893,7 @@ var TITLES = {
848
893
  "chap.task.complete": "Submit task output",
849
894
  "chap.audit.read": "Read audit log",
850
895
  "chap.review.request": "Request review",
896
+ "chap.review.acknowledge": "Acknowledge review",
851
897
  "chap.decide.approve": "Approve",
852
898
  "chap.decide.reject": "Reject",
853
899
  "chap.decide.override": "Override with a correction",
@@ -888,6 +934,19 @@ var TOOL_ANNOTATIONS = Object.fromEntries(
888
934
  openWorldHint: OPEN_WORLD.has(name)
889
935
  }])
890
936
  );
937
+ var AGENT_MCP_TOOLS = /* @__PURE__ */ new Set([
938
+ "chap.task.create",
939
+ "chap.task.update",
940
+ "chap.task.complete",
941
+ "chap.whisper.ask",
942
+ "chap.decide.approve",
943
+ "chap.decide.reject",
944
+ "chap.decide.override",
945
+ "chap.review.acknowledge",
946
+ "chap.workspace.describe",
947
+ "chap.audit.read",
948
+ "chap.audit.verify_chain"
949
+ ]);
891
950
 
892
951
  // src/envelope.ts
893
952
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -939,6 +998,73 @@ function readMeta(params) {
939
998
  return meta;
940
999
  }
941
1000
 
1001
+ // src/remote.ts
1002
+ var REMOTE_TIMEOUT_MS = 3e4;
1003
+ var ChapEndpointError = class extends Error {
1004
+ constructor(message) {
1005
+ super(message);
1006
+ this.name = "ChapEndpointError";
1007
+ }
1008
+ };
1009
+ function remoteDispatcher(endpoint, timeoutMs = REMOTE_TIMEOUT_MS) {
1010
+ return {
1011
+ async dispatch(envelope) {
1012
+ let response;
1013
+ let text;
1014
+ try {
1015
+ response = await fetch(endpoint, {
1016
+ method: "POST",
1017
+ headers: { "content-type": "application/json", accept: "application/json" },
1018
+ body: JSON.stringify(envelope),
1019
+ signal: AbortSignal.timeout(timeoutMs)
1020
+ });
1021
+ text = await response.text();
1022
+ } catch (err) {
1023
+ throw new ChapEndpointError(`The CHAP server at ${endpoint} could not be reached (${reason(err)}). Start it, or check --url.`);
1024
+ }
1025
+ let body;
1026
+ try {
1027
+ body = JSON.parse(text);
1028
+ } catch {
1029
+ throw new ChapEndpointError(`The CHAP server at ${endpoint} answered HTTP ${response.status} with a body that is not JSON.`);
1030
+ }
1031
+ if (isResponseEnvelope(body)) return body;
1032
+ const said = typeof body?.error === "string" ? `: ${body.error}` : "";
1033
+ throw new ChapEndpointError(`The CHAP server at ${endpoint} refused the call with HTTP ${response.status}${said}`);
1034
+ }
1035
+ };
1036
+ }
1037
+ function reason(err) {
1038
+ if (err instanceof Error && err.name === "TimeoutError") return "timed out";
1039
+ const cause = err.cause;
1040
+ if (typeof cause?.code === "string") return cause.code;
1041
+ if (typeof cause?.message === "string" && cause.message) return cause.message;
1042
+ return err instanceof Error ? err.message : String(err);
1043
+ }
1044
+ function isResponseEnvelope(body) {
1045
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return false;
1046
+ const b = body;
1047
+ return b.jsonrpc === "2.0" && ("result" in b || typeof b.error === "object" && b.error !== null);
1048
+ }
1049
+
1050
+ // src/http.ts
1051
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1052
+ var import_types2 = require("@modelcontextprotocol/sdk/types.js");
1053
+ var HEADER_MISMATCH = -32020;
1054
+ var DEFAULT_MAX_BODY_BYTES = 1048576 + 65536;
1055
+ var HANDSHAKE_VERSIONS = import_types2.SUPPORTED_PROTOCOL_VERSIONS.filter(
1056
+ (v) => !MODERN_PROTOCOL_VERSIONS.includes(v)
1057
+ );
1058
+ var STATUS_FOR_CODE = /* @__PURE__ */ new Map([
1059
+ [import_types2.ErrorCode.ParseError, 400],
1060
+ [import_types2.ErrorCode.InvalidRequest, 400],
1061
+ [import_types2.ErrorCode.InvalidParams, 400],
1062
+ [HEADER_MISMATCH, 400],
1063
+ [UNSUPPORTED_PROTOCOL_VERSION, 400],
1064
+ [import_types2.ErrorCode.MethodNotFound, 404]
1065
+ ]);
1066
+ var INTERNAL = { code: import_types2.ErrorCode.InternalError, message: "Internal error" };
1067
+
942
1068
  // src/index.ts
943
1069
  var META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
944
1070
  var LIST_TTL_MS = 36e5;
@@ -949,10 +1075,32 @@ var ServerDiscoverRequestSchema = import_zod.z.object({
949
1075
  // exist in zod 3, where it stopped the package at import.
950
1076
  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())
951
1077
  });
1078
+ var INTERNAL_ERROR = -32603;
1079
+ var FAILED_INSIDE = { chap_error: INTERNAL_ERROR, message: "The CHAP call failed inside the server; its log has the detail." };
1080
+ function refusePerson(participant) {
1081
+ if (participant?.startsWith("human:")) {
1082
+ 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.`);
1083
+ }
1084
+ }
1085
+ function pinnedSchema(schema, participant) {
1086
+ if (!schema.properties?.from) return schema;
1087
+ return {
1088
+ ...schema,
1089
+ properties: {
1090
+ ...schema.properties,
1091
+ from: {
1092
+ type: "string",
1093
+ description: `This server acts as ${participant}: it fills this in, and refuses a call that names anyone else.`
1094
+ }
1095
+ },
1096
+ required: (schema.required ?? []).filter((key) => key !== "from")
1097
+ };
1098
+ }
952
1099
  function makeChapMcpServer(coord, options = {}) {
1100
+ refusePerson(options.participant);
953
1101
  const serverInfo = {
954
1102
  name: options.name ?? "chap",
955
- version: options.version ?? "0.3.4"
1103
+ version: options.version ?? "0.3.5"
956
1104
  };
957
1105
  const server = new import_server.Server(
958
1106
  serverInfo,
@@ -973,13 +1121,13 @@ function makeChapMcpServer(coord, options = {}) {
973
1121
  // not already see. The annotation carries a readable one.
974
1122
  title: ann?.title ?? name,
975
1123
  description: TOOL_DESCRIPTIONS[name] ?? `CHAP method ${methodForTool(name)}.`,
976
- inputSchema: SCHEMAS[name],
1124
+ inputSchema: options.participant === void 0 ? SCHEMAS[name] : pinnedSchema(SCHEMAS[name], options.participant),
977
1125
  // Behavioural hints. Without these a client has only the prose to go
978
1126
  // on when deciding whether a call is safe to make or to retry.
979
1127
  ...ann ? { annotations: ann } : {}
980
1128
  };
981
1129
  });
982
- server.setRequestHandler(import_types2.ListToolsRequestSchema, async (request) => {
1130
+ server.setRequestHandler(import_types3.ListToolsRequestSchema, async (request) => {
983
1131
  const era = classifyEnvelope(request.params);
984
1132
  if (era === "legacy") return { tools: enabledTools };
985
1133
  return {
@@ -992,7 +1140,7 @@ function makeChapMcpServer(coord, options = {}) {
992
1140
  });
993
1141
  server.setRequestHandler(ServerDiscoverRequestSchema, async (request) => {
994
1142
  if (classifyEnvelope(request.params) === "legacy") {
995
- throw new ProtocolError(import_types2.ErrorCode.MethodNotFound, "Method not found", "server/discover");
1143
+ throw new ProtocolError(import_types3.ErrorCode.MethodNotFound, "Method not found", "server/discover");
996
1144
  }
997
1145
  return {
998
1146
  resultType: "complete",
@@ -1004,18 +1152,15 @@ function makeChapMcpServer(coord, options = {}) {
1004
1152
  _meta: { [META_SERVER_INFO]: serverInfo }
1005
1153
  };
1006
1154
  });
1007
- server.setRequestHandler(import_types2.CallToolRequestSchema, async (request) => {
1155
+ server.setRequestHandler(import_types3.CallToolRequestSchema, async (request) => {
1008
1156
  const era = classifyEnvelope(request.params);
1009
1157
  const terminal = (result) => era === "modern" ? { resultType: "complete", ...result } : result;
1010
1158
  const toolName = request.params.name;
1011
1159
  const rawArgs = request.params.arguments ?? {};
1012
- const method = methodForTool(toolName);
1013
- if (!method) {
1014
- return terminal({
1015
- isError: true,
1016
- content: [{ type: "text", text: `Unknown CHAP tool: ${toolName}` }]
1017
- });
1160
+ if (!Object.hasOwn(SCHEMAS, toolName)) {
1161
+ throw new ProtocolError(import_types3.ErrorCode.InvalidParams, `Unknown tool: ${toolName}`);
1018
1162
  }
1163
+ const method = methodForTool(toolName);
1019
1164
  if (!filter(toolName)) {
1020
1165
  return terminal({
1021
1166
  isError: true,
@@ -1043,20 +1188,25 @@ function makeChapMcpServer(coord, options = {}) {
1043
1188
  };
1044
1189
  let response;
1045
1190
  try {
1046
- response = coord.dispatch(envelope);
1191
+ response = await coord.dispatch(envelope);
1047
1192
  } catch (e) {
1048
- const msg = e instanceof Error ? e.message : String(e);
1193
+ if (e instanceof ChapEndpointError) {
1194
+ return terminal({ isError: true, content: [{ type: "text", text: e.message }] });
1195
+ }
1196
+ console.error("CHAP dispatch failed:", e);
1049
1197
  return terminal({
1050
1198
  isError: true,
1051
- content: [{ type: "text", text: `CHAP dispatch threw: ${msg}` }]
1199
+ content: [{ type: "text", text: FAILED_INSIDE.message }]
1052
1200
  });
1053
1201
  }
1054
1202
  if (response.error) {
1203
+ const internal = response.error.code === INTERNAL_ERROR;
1204
+ if (internal) console.error("CHAP call failed:", response.error);
1055
1205
  return terminal({
1056
1206
  isError: true,
1057
1207
  content: [{
1058
1208
  type: "text",
1059
- text: JSON.stringify({
1209
+ text: JSON.stringify(internal ? FAILED_INSIDE : {
1060
1210
  chap_error: response.error.code,
1061
1211
  message: response.error.message,
1062
1212
  ...response.error.data !== void 0 ? { data: response.error.data } : {}
@@ -1075,7 +1225,7 @@ function makeChapMcpServer(coord, options = {}) {
1075
1225
  }
1076
1226
 
1077
1227
  // src/cli.ts
1078
- var VERSION = "0.3.4";
1228
+ var VERSION = "0.3.5";
1079
1229
  var DEFAULT_PROFILES = [
1080
1230
  "core/1.0",
1081
1231
  "review/1.0",
@@ -1087,41 +1237,167 @@ var DEFAULT_PROFILES = [
1087
1237
  "modes/1.0",
1088
1238
  "audit-scitt/1.0"
1089
1239
  ];
1090
- var profiles = process.env.CHAP_PROFILES ? process.env.CHAP_PROFILES.split(",").map((p) => p.trim()).filter(Boolean) : DEFAULT_PROFILES;
1091
- if (profiles.length === 0) {
1092
- process.stderr.write("CHAP_PROFILES was set but empty. Unset it to use the defaults.\n");
1093
- process.exit(1);
1240
+ var DEFAULT_PARTICIPANT = "agent:assistant";
1241
+ var USAGE = `Usage: chap-mcp-server [options]
1242
+
1243
+ Serves CHAP to one MCP client on stdio, making every call as one participant.
1244
+
1245
+ Options:
1246
+ --as <uri> the participant every call is made as (CHAP_PARTICIPANT,
1247
+ default ${DEFAULT_PARTICIPANT}). A human: URI is refused:
1248
+ a person decides in a desk, never through the assistant
1249
+ --tools agent|all agent, the default: the tools an agent's own work needs,
1250
+ listed below; all: every CHAP method
1251
+ --url <endpoint> forward every call to a running CHAP server's JSON-RPC
1252
+ endpoint, such as http://127.0.0.1:8792/chap, which
1253
+ keeps the workspaces
1254
+ --db <path> keep the workspaces in this SQLite file (CHAP_DB_PATH);
1255
+ a leading ~ is the home directory. Without --db or
1256
+ --url, state is in memory only
1257
+ --profiles <list> comma-separated profiles for new workspaces
1258
+ (CHAP_PROFILES, default those listed below)
1259
+ --help print this and exit
1260
+
1261
+ The agent tools:
1262
+ ${[...AGENT_MCP_TOOLS].sort().map((name) => ` ${name}`).join("\n")}
1263
+
1264
+ The default profiles:
1265
+ ${DEFAULT_PROFILES.map((profile) => ` ${profile}`).join("\n")}
1266
+ `;
1267
+ var URI_RE = /^[a-z][a-z0-9+.-]*:[\x21\x23-\x5b\x5d-\x7e]+$/;
1268
+ var UsageError = class extends Error {
1269
+ };
1270
+ function fromEnv(name) {
1271
+ const value = process.env[name]?.trim();
1272
+ return value ? value : void 0;
1273
+ }
1274
+ function expandHome(path) {
1275
+ return /^~(?=$|[\\/])/.test(path) ? (0, import_node_os.homedir)() + path.slice(1) : path;
1276
+ }
1277
+ function parseArgs(argv) {
1278
+ const given = {};
1279
+ let help = false;
1280
+ for (let i = 0; i < argv.length; i++) {
1281
+ const arg = argv[i];
1282
+ if (arg === "--help" || arg === "-h") {
1283
+ help = true;
1284
+ continue;
1285
+ }
1286
+ const m = /^(--as|--tools|--url|--db|--profiles)(?:=(.*))?$/.exec(arg);
1287
+ if (!m) throw new UsageError(arg.startsWith("-") ? `Unknown option ${arg}` : `Unexpected argument ${arg}`);
1288
+ const value = m[2] ?? argv[++i];
1289
+ if (value === void 0 || value === "") throw new UsageError(`${m[1]} needs a value`);
1290
+ given[m[1]] = value;
1291
+ }
1292
+ const [participant, participantFrom] = given["--as"] !== void 0 ? [given["--as"], "--as"] : fromEnv("CHAP_PARTICIPANT") !== void 0 ? [fromEnv("CHAP_PARTICIPANT"), "CHAP_PARTICIPANT"] : [DEFAULT_PARTICIPANT, "--as"];
1293
+ if (!URI_RE.test(participant)) {
1294
+ throw new UsageError(`${participantFrom} ${participant} is not a participant URI. Use the form agent:name@example.org.`);
1295
+ }
1296
+ if (participant.startsWith("human:")) {
1297
+ throw new UsageError(
1298
+ `${participantFrom} ${participant} names a person. This server acts for an assistant, and a person decides in a desk; give the assistant's own URI, such as agent:assistant.`
1299
+ );
1300
+ }
1301
+ const tools = given["--tools"] ?? "agent";
1302
+ if (tools !== "agent" && tools !== "all") throw new UsageError(`--tools takes agent or all; ${tools} is neither`);
1303
+ let url;
1304
+ if (given["--url"] !== void 0) {
1305
+ let parsed;
1306
+ try {
1307
+ parsed = new URL(given["--url"]);
1308
+ } catch {
1309
+ parsed = void 0;
1310
+ }
1311
+ if (!parsed || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1312
+ throw new UsageError(`--url takes the http or https address of a CHAP JSON-RPC endpoint, such as http://127.0.0.1:8792/chap; ${given["--url"]} is not one`);
1313
+ }
1314
+ url = parsed.href;
1315
+ }
1316
+ const [rawDb, dbFrom] = given["--db"] !== void 0 ? [given["--db"], "--db"] : [fromEnv("CHAP_DB_PATH"), "CHAP_DB_PATH"];
1317
+ const [rawProfiles, profilesFrom] = given["--profiles"] !== void 0 ? [given["--profiles"], "--profiles"] : [fromEnv("CHAP_PROFILES"), "CHAP_PROFILES"];
1318
+ if (url && rawDb !== void 0) {
1319
+ throw new UsageError(`--url forwards to a server that keeps its own store, so ${dbFrom} does not apply. Set one or the other.`);
1320
+ }
1321
+ if (url && rawProfiles !== void 0) {
1322
+ throw new UsageError(`--url forwards to a server that sets its own profiles, so ${profilesFrom} does not apply. Set one or the other.`);
1323
+ }
1324
+ let profiles = DEFAULT_PROFILES;
1325
+ if (rawProfiles !== void 0) {
1326
+ profiles = rawProfiles.split(",").map((p) => p.trim()).filter(Boolean);
1327
+ if (profiles.length === 0) throw new UsageError(`${profilesFrom} names no profiles. Leave it unset for the defaults.`);
1328
+ }
1329
+ return {
1330
+ help,
1331
+ participant,
1332
+ tools,
1333
+ url,
1334
+ profiles,
1335
+ ...rawDb !== void 0 ? { db: (0, import_node_path.resolve)(expandHome(rawDb)) } : {}
1336
+ };
1094
1337
  }
1095
1338
  async function openStore(dbPath) {
1096
1339
  try {
1340
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(dbPath), { recursive: true });
1097
1341
  const { SqliteStore } = await import("@brightbeamai/chap-coordinator/storage/sqlite");
1098
1342
  return new SqliteStore(dbPath);
1099
1343
  } catch (err) {
1100
1344
  const detail = err instanceof Error ? err.message : String(err);
1101
1345
  process.stderr.write(
1102
- `CHAP_DB_PATH is set to ${dbPath} but the SQLite store could not be opened.
1346
+ `The SQLite store at ${dbPath} could not be opened.
1103
1347
  ${detail}
1104
1348
  better-sqlite3 is an optional native dependency; reinstall with build tools
1105
- available, or unset CHAP_DB_PATH to run in memory without persistence.
1349
+ available, or leave --db and CHAP_DB_PATH unset to run in memory without persistence.
1106
1350
  `
1107
1351
  );
1108
1352
  process.exit(1);
1109
1353
  }
1110
1354
  }
1111
1355
  async function main() {
1112
- const dbPath = process.env.CHAP_DB_PATH?.trim();
1113
- const store = dbPath ? await openStore(dbPath) : void 0;
1114
- const coord = new import_chap_coordinator.Coordinator({ defaultProfiles: profiles, ...store ? { store } : {} });
1115
- await coord.start();
1116
- const server = makeChapMcpServer(coord, { name: "chap", version: VERSION });
1117
- process.stderr.write(`CHAP MCP server ${VERSION} on stdio.
1356
+ let options;
1357
+ try {
1358
+ options = parseArgs(process.argv.slice(2));
1359
+ } catch (err) {
1360
+ if (!(err instanceof UsageError)) throw err;
1361
+ process.stderr.write(`${err.message}
1362
+ Run chap-mcp-server --help for the options.
1118
1363
  `);
1119
- process.stderr.write(`Profiles: ${profiles.join(", ")}
1364
+ process.exit(2);
1365
+ }
1366
+ if (options.help) {
1367
+ process.stdout.write(USAGE);
1368
+ return;
1369
+ }
1370
+ const toolSet = options.tools === "agent" ? "the agent tools" : "every CHAP tool";
1371
+ process.stderr.write(`CHAP MCP server ${VERSION} on stdio, acting as ${options.participant} with ${toolSet}.
1120
1372
  `);
1121
- process.stderr.write(
1122
- dbPath ? `Persisting to ${dbPath}
1123
- ` : "In memory. Set CHAP_DB_PATH to keep workspaces across restarts.\n"
1124
- );
1373
+ let dispatcher;
1374
+ if (options.url) {
1375
+ dispatcher = remoteDispatcher(options.url);
1376
+ process.stderr.write(`Forwarding every call to ${options.url}, whose coordinator keeps the workspaces.
1377
+ `);
1378
+ } else {
1379
+ const store = options.db ? await openStore(options.db) : void 0;
1380
+ const coord = new import_chap_coordinator.Coordinator({ defaultProfiles: options.profiles, ...store ? { store } : {} });
1381
+ await coord.start();
1382
+ dispatcher = coord;
1383
+ process.stderr.write(`Profiles: ${options.profiles.join(", ")}
1384
+ `);
1385
+ process.stderr.write(
1386
+ options.db ? `Persisting to ${options.db}
1387
+ ` : "State is in memory only: the workspaces are lost when this process exits. Set --db or CHAP_DB_PATH to keep them.\n"
1388
+ );
1389
+ if (options.tools === "agent" && coord.workspaces.size === 0) {
1390
+ process.stderr.write(
1391
+ "No workspace exists yet, and the agent tools cannot create one: run once with --tools all, or use --url to reach a server that has one.\n"
1392
+ );
1393
+ }
1394
+ }
1395
+ const server = makeChapMcpServer(dispatcher, {
1396
+ name: "chap",
1397
+ version: VERSION,
1398
+ participant: options.participant,
1399
+ ...options.tools === "agent" ? { toolFilter: (name) => AGENT_MCP_TOOLS.has(name) } : {}
1400
+ });
1125
1401
  await server.connect(new import_stdio.StdioServerTransport());
1126
1402
  }
1127
1403
  main().catch((err) => {