@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/README.md +162 -19
- package/dist/cli.cjs +319 -43
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -41
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +466 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -10
- package/dist/index.d.ts +84 -10
- package/dist/index.js +467 -23
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +40 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +40 -3
- package/dist/schemas.js.map +1 -1
- package/dist/tools.cjs +27 -6
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +17 -1
- package/dist/tools.d.ts +17 -1
- package/dist/tools.js +26 -6
- package/dist/tools.js.map +1 -1
- package/package.json +3 -3
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
|
|
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"
|
|
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'
|
|
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
|
|
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,6 +1011,335 @@ 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;
|
|
@@ -963,10 +1350,32 @@ var ServerDiscoverRequestSchema = import_zod.z.object({
|
|
|
963
1350
|
// exist in zod 3, where it stopped the package at import.
|
|
964
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())
|
|
965
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
|
+
}
|
|
966
1374
|
function makeChapMcpServer(coord, options = {}) {
|
|
1375
|
+
refusePerson(options.participant);
|
|
967
1376
|
const serverInfo = {
|
|
968
1377
|
name: options.name ?? "chap",
|
|
969
|
-
version: options.version ?? "0.3.
|
|
1378
|
+
version: options.version ?? "0.3.5"
|
|
970
1379
|
};
|
|
971
1380
|
const server = new import_server.Server(
|
|
972
1381
|
serverInfo,
|
|
@@ -987,13 +1396,13 @@ function makeChapMcpServer(coord, options = {}) {
|
|
|
987
1396
|
// not already see. The annotation carries a readable one.
|
|
988
1397
|
title: ann?.title ?? name,
|
|
989
1398
|
description: TOOL_DESCRIPTIONS[name] ?? `CHAP method ${methodForTool(name)}.`,
|
|
990
|
-
inputSchema: SCHEMAS[name],
|
|
1399
|
+
inputSchema: options.participant === void 0 ? SCHEMAS[name] : pinnedSchema(SCHEMAS[name], options.participant),
|
|
991
1400
|
// Behavioural hints. Without these a client has only the prose to go
|
|
992
1401
|
// on when deciding whether a call is safe to make or to retry.
|
|
993
1402
|
...ann ? { annotations: ann } : {}
|
|
994
1403
|
};
|
|
995
1404
|
});
|
|
996
|
-
server.setRequestHandler(
|
|
1405
|
+
server.setRequestHandler(import_types3.ListToolsRequestSchema, async (request) => {
|
|
997
1406
|
const era = classifyEnvelope(request.params);
|
|
998
1407
|
if (era === "legacy") return { tools: enabledTools };
|
|
999
1408
|
return {
|
|
@@ -1006,7 +1415,7 @@ function makeChapMcpServer(coord, options = {}) {
|
|
|
1006
1415
|
});
|
|
1007
1416
|
server.setRequestHandler(ServerDiscoverRequestSchema, async (request) => {
|
|
1008
1417
|
if (classifyEnvelope(request.params) === "legacy") {
|
|
1009
|
-
throw new ProtocolError(
|
|
1418
|
+
throw new ProtocolError(import_types3.ErrorCode.MethodNotFound, "Method not found", "server/discover");
|
|
1010
1419
|
}
|
|
1011
1420
|
return {
|
|
1012
1421
|
resultType: "complete",
|
|
@@ -1018,18 +1427,15 @@ function makeChapMcpServer(coord, options = {}) {
|
|
|
1018
1427
|
_meta: { [META_SERVER_INFO]: serverInfo }
|
|
1019
1428
|
};
|
|
1020
1429
|
});
|
|
1021
|
-
server.setRequestHandler(
|
|
1430
|
+
server.setRequestHandler(import_types3.CallToolRequestSchema, async (request) => {
|
|
1022
1431
|
const era = classifyEnvelope(request.params);
|
|
1023
1432
|
const terminal = (result) => era === "modern" ? { resultType: "complete", ...result } : result;
|
|
1024
1433
|
const toolName = request.params.name;
|
|
1025
1434
|
const rawArgs = request.params.arguments ?? {};
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
return terminal({
|
|
1029
|
-
isError: true,
|
|
1030
|
-
content: [{ type: "text", text: `Unknown CHAP tool: ${toolName}` }]
|
|
1031
|
-
});
|
|
1435
|
+
if (!Object.hasOwn(SCHEMAS, toolName)) {
|
|
1436
|
+
throw new ProtocolError(import_types3.ErrorCode.InvalidParams, `Unknown tool: ${toolName}`);
|
|
1032
1437
|
}
|
|
1438
|
+
const method = methodForTool(toolName);
|
|
1033
1439
|
if (!filter(toolName)) {
|
|
1034
1440
|
return terminal({
|
|
1035
1441
|
isError: true,
|
|
@@ -1057,20 +1463,25 @@ function makeChapMcpServer(coord, options = {}) {
|
|
|
1057
1463
|
};
|
|
1058
1464
|
let response;
|
|
1059
1465
|
try {
|
|
1060
|
-
response = coord.dispatch(envelope);
|
|
1466
|
+
response = await coord.dispatch(envelope);
|
|
1061
1467
|
} catch (e) {
|
|
1062
|
-
|
|
1468
|
+
if (e instanceof ChapEndpointError) {
|
|
1469
|
+
return terminal({ isError: true, content: [{ type: "text", text: e.message }] });
|
|
1470
|
+
}
|
|
1471
|
+
console.error("CHAP dispatch failed:", e);
|
|
1063
1472
|
return terminal({
|
|
1064
1473
|
isError: true,
|
|
1065
|
-
content: [{ type: "text", text:
|
|
1474
|
+
content: [{ type: "text", text: FAILED_INSIDE.message }]
|
|
1066
1475
|
});
|
|
1067
1476
|
}
|
|
1068
1477
|
if (response.error) {
|
|
1478
|
+
const internal = response.error.code === INTERNAL_ERROR;
|
|
1479
|
+
if (internal) console.error("CHAP call failed:", response.error);
|
|
1069
1480
|
return terminal({
|
|
1070
1481
|
isError: true,
|
|
1071
1482
|
content: [{
|
|
1072
1483
|
type: "text",
|
|
1073
|
-
text: JSON.stringify({
|
|
1484
|
+
text: JSON.stringify(internal ? FAILED_INSIDE : {
|
|
1074
1485
|
chap_error: response.error.code,
|
|
1075
1486
|
message: response.error.message,
|
|
1076
1487
|
...response.error.data !== void 0 ? { data: response.error.data } : {}
|
|
@@ -1103,8 +1514,37 @@ function dispatchToolCall(coord, toolName, args, envelopeId = "mcp-call") {
|
|
|
1103
1514
|
params: coerceToolArgs(toolName, args ?? {})
|
|
1104
1515
|
});
|
|
1105
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
|
+
}
|
|
1106
1545
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1107
1546
|
0 && (module.exports = {
|
|
1547
|
+
AGENT_MCP_TOOLS,
|
|
1108
1548
|
META_CLIENT_CAPABILITIES,
|
|
1109
1549
|
META_CLIENT_INFO,
|
|
1110
1550
|
META_PROTOCOL_VERSION,
|
|
@@ -1118,6 +1558,7 @@ function dispatchToolCall(coord, toolName, args, envelopeId = "mcp-call") {
|
|
|
1118
1558
|
TOOL_DESCRIPTIONS,
|
|
1119
1559
|
TOOL_NAMES,
|
|
1120
1560
|
UNSUPPORTED_PROTOCOL_VERSION,
|
|
1561
|
+
chapMcpHttpHandler,
|
|
1121
1562
|
classifyEnvelope,
|
|
1122
1563
|
coerceToolArgs,
|
|
1123
1564
|
dispatchToolCall,
|