@principles/pd-mcp-server 0.0.0-stage → 0.2.0
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/LICENSE +73 -0
- package/NOTICE +2 -0
- package/README.md +30 -2
- package/dist/budget.d.ts +15 -0
- package/dist/budget.js +19 -0
- package/dist/deadline.d.ts +75 -0
- package/dist/deadline.js +136 -0
- package/dist/envelope.d.ts +52 -0
- package/dist/envelope.js +63 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +76 -0
- package/dist/logging.d.ts +12 -0
- package/dist/logging.js +42 -0
- package/dist/server.d.ts +90 -0
- package/dist/server.js +178 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +13 -0
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.js +77 -0
- package/package.json +48 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
10
|
+
|
|
11
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
12
|
+
|
|
13
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
14
|
+
|
|
15
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
16
|
+
|
|
17
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
18
|
+
|
|
19
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
20
|
+
|
|
21
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
22
|
+
|
|
23
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
24
|
+
|
|
25
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
26
|
+
|
|
27
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
28
|
+
|
|
29
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
30
|
+
|
|
31
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
32
|
+
|
|
33
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
34
|
+
|
|
35
|
+
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
36
|
+
|
|
37
|
+
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
38
|
+
|
|
39
|
+
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
40
|
+
|
|
41
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
42
|
+
|
|
43
|
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
44
|
+
|
|
45
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
46
|
+
|
|
47
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
48
|
+
|
|
49
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
50
|
+
|
|
51
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
52
|
+
|
|
53
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
54
|
+
|
|
55
|
+
END OF TERMS AND CONDITIONS
|
|
56
|
+
|
|
57
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
58
|
+
|
|
59
|
+
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
|
60
|
+
|
|
61
|
+
Copyright [yyyy] [name of copyright owner]
|
|
62
|
+
|
|
63
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
64
|
+
you may not use this file except in compliance with the License.
|
|
65
|
+
You may obtain a copy of the License at
|
|
66
|
+
|
|
67
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
68
|
+
|
|
69
|
+
Unless required by applicable law or agreed to in writing, software
|
|
70
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
71
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
72
|
+
See the License for the specific language governing permissions and
|
|
73
|
+
limitations under the License.
|
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @principles/pd-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Principles Disciple 的正式 MCP Server —— 三个宿主(Codex、OpenClaw、WorkBuddy)共享的**同一份** Server 实现(SPEC-PD-MCP-GATE4-001)。每个宿主以插件内置 MCP 配置启动自己的 Server 子进程,业务数据**只**经 Gate 3 Capability(`@principles/core/runtime-v2/pain-recall-capability`,OD-A v1 披露)读取。
|
|
4
|
+
|
|
5
|
+
## 唯一 Tool:`pd_recall_principle_pains`(只读)
|
|
6
|
+
|
|
7
|
+
| 契约 | 值 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 输入 | `principleId`(1..200,必填)、`limit`(1..10,默认 3);其余属性一律拒绝(协议层校验 + Capability 层复验) |
|
|
10
|
+
| 输出 | 单个 text content block,携带 `pd.capability.v1` JSON;不使用 structuredContent |
|
|
11
|
+
| 响应预算 | ≤ 32768 UTF-8 bytes(32768 通过 / 32769 拒绝 → `unavailable` + `response_budget_exceeded`,`isError:true`,绝不静默截断) |
|
|
12
|
+
| 调用截止 | 10 秒(handler 起算,含校验/Capability/投影/序列化;超时 = `tool_execution_failed`,迟到结果抑制;不宣称底层可取消) |
|
|
13
|
+
| isError=true | 仅两类:披露断言失败;Adapter 执行失败(预算超限/超时/异常/关闭中)——普通业务状态一律非错误 |
|
|
14
|
+
| 进程边界 | stdout 仅 MCP 协议消息;日志走 stderr(结构化脱敏 JSON);无常驻 daemon |
|
|
15
|
+
|
|
16
|
+
## 工作区绑定(fail-closed)
|
|
17
|
+
|
|
18
|
+
业务 `workspaceDir` 是**启动配置**,绝不出现在 Tool 参数中。它由宿主插件的 PD 自有 `mcp-binding.json`(经 `launcher.cjs`)注入为环境变量 `PD_MCP_WORKSPACE_DIR`(可选 `PD_MCP_HOST` ∈ codex|openclaw|workbuddy)。绑定缺失/相对/不存在 → 启动即失败(stderr 一行结构化原因 + exit 1),**没有任何** `process.cwd()`/`HOME` 回退。
|
|
19
|
+
|
|
20
|
+
## 架构守卫
|
|
21
|
+
|
|
22
|
+
`src/__tests__/architecture-guards.test.ts` 机械保证:Core 零 MCP SDK 依赖;本包仅经 Capability 公共子路径访问业务;无 CLI 子进程查询;pd-cli 与本包互不依赖;SDK 精确锁版(`@modelcontextprotocol/server@2.4.0`)。
|
|
23
|
+
|
|
24
|
+
## 构建 / 测试
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run build # tsc → dist/
|
|
28
|
+
npm test # build + vitest(协议契约 / 预算边界 / deadline / 绑定 / 真实进程 stdio / 架构守卫)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
发布随 PD release 列车(changesets);SDK 与 zod 均为精确锁版。
|
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response budget — SPEC-PD-MCP-GATE4-001 §4.6 (Gate 4 Adapter responsibility;
|
|
3
|
+
* NOT a Core Capability constraint).
|
|
4
|
+
*
|
|
5
|
+
* The single text content block carries the `pd.capability.v1` JSON. Its
|
|
6
|
+
* UTF-8 byte size must be ≤ 32768: 32768 passes, 32769 is over budget.
|
|
7
|
+
* Over budget is a bounded failure envelope (`unavailable` +
|
|
8
|
+
* `response_budget_exceeded`), never a silent truncation, never a field-drop
|
|
9
|
+
* disguised as success. The failure envelope itself must fit the budget.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RESPONSE_BUDGET_BYTES: 32768;
|
|
12
|
+
/** UTF-8 byte length of the exact string that will be placed in the text
|
|
13
|
+
* content block — the measure the contract is written against. */
|
|
14
|
+
export declare function measureTextBytes(text: string): number;
|
|
15
|
+
export declare function withinBudget(text: string): boolean;
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response budget — SPEC-PD-MCP-GATE4-001 §4.6 (Gate 4 Adapter responsibility;
|
|
3
|
+
* NOT a Core Capability constraint).
|
|
4
|
+
*
|
|
5
|
+
* The single text content block carries the `pd.capability.v1` JSON. Its
|
|
6
|
+
* UTF-8 byte size must be ≤ 32768: 32768 passes, 32769 is over budget.
|
|
7
|
+
* Over budget is a bounded failure envelope (`unavailable` +
|
|
8
|
+
* `response_budget_exceeded`), never a silent truncation, never a field-drop
|
|
9
|
+
* disguised as success. The failure envelope itself must fit the budget.
|
|
10
|
+
*/
|
|
11
|
+
export const RESPONSE_BUDGET_BYTES = 32768;
|
|
12
|
+
/** UTF-8 byte length of the exact string that will be placed in the text
|
|
13
|
+
* content block — the measure the contract is written against. */
|
|
14
|
+
export function measureTextBytes(text) {
|
|
15
|
+
return Buffer.byteLength(text, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
export function withinBudget(text) {
|
|
18
|
+
return measureTextBytes(text) <= RESPONSE_BUDGET_BYTES;
|
|
19
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call deadline — SPEC-PD-MCP-GATE4-001 §4.7 (Gate 4 Adapter responsibility).
|
|
3
|
+
*
|
|
4
|
+
* Every tool call gets at most `deadlineMs` (production: 10 s) counted from
|
|
5
|
+
* handler entry, covering input validation, capability call, projection and
|
|
6
|
+
* JSON serialization. The Gate 3 Capability exposes no AbortSignal, so this
|
|
7
|
+
* is HONEST non-cancellation: on expiry we stop waiting, suppress the late
|
|
8
|
+
* result (a timeout must never later be reported as success), and track the
|
|
9
|
+
* still-running operation in a bounded orphan set whose eventual settlement
|
|
10
|
+
* is observed — proving resources are released (§4.7). `Promise.race` here is
|
|
11
|
+
* explicitly NOT claimed as underlying cancellation.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_DEADLINE_MS: 10000;
|
|
14
|
+
/** Upper bound on concurrently EXECUTING capability calls (§4.7). Admission
|
|
15
|
+
* is reserved synchronously before the capability is called, so the bound is
|
|
16
|
+
* real even when every in-flight call later outlives its deadline. Small by
|
|
17
|
+
* design: a read-only recall that takes >10 s is already pathological; four
|
|
18
|
+
* concurrent ones is a stuck host, not load. */
|
|
19
|
+
export declare const MAX_CONCURRENT_CALLS: 4;
|
|
20
|
+
/** Upper bound on still-running timed-out operations. Orphans are a subset of
|
|
21
|
+
* admitted calls; this registry keeps their settlement observable even after
|
|
22
|
+
* the caller stopped waiting (§4.7 “限制仍在运行的超时任务数量”). */
|
|
23
|
+
export declare const MAX_TRACKED_ORPHANS: 4;
|
|
24
|
+
export type DeadlineOutcome<T> = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly value: T;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly timedOut: true;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Run `fn` under a deadline. On timeout the returned promise settles
|
|
33
|
+
* `{ timedOut }` immediately while `fn`'s promise keeps running; it is handed
|
|
34
|
+
* to `onOrphan` (the caller registers it in the orphan limiter). If
|
|
35
|
+
* `onOrphan` returns false (limiter saturated), the caller must refuse new
|
|
36
|
+
* work — the operation is still tracked for settlement by the race itself.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runWithDeadline<T>(fn: () => Promise<T>, deadlineMs: number, onOrphan?: (orphaned: Promise<T>) => boolean): Promise<DeadlineOutcome<T>>;
|
|
39
|
+
/**
|
|
40
|
+
* Bounded registry of still-running timed-out operations. `register` refuses
|
|
41
|
+
* (returns false) once `max` orphans are unsettled; settled promises remove
|
|
42
|
+
* themselves, so the limit reflects *currently running* stragglers only.
|
|
43
|
+
*/
|
|
44
|
+
export declare class OrphanLimiter {
|
|
45
|
+
#private;
|
|
46
|
+
constructor(max?: number);
|
|
47
|
+
get size(): number;
|
|
48
|
+
get saturated(): boolean;
|
|
49
|
+
register(orphaned: Promise<unknown>): boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Synchronous admission semaphore for executing capability calls (§4.7).
|
|
53
|
+
*
|
|
54
|
+
* The previous design only counted already-timed-out stragglers, so N requests
|
|
55
|
+
* could all pass the saturation check before any of them timed out and then
|
|
56
|
+
* start N > limit capability calls. Admission is now reserved at handler entry
|
|
57
|
+
* — BEFORE the capability is invoked — and released only when the underlying
|
|
58
|
+
* work actually settles (normal completion, exception, or an orphaned call
|
|
59
|
+
* that outlived its deadline). A timeout therefore stops the WAIT, never the
|
|
60
|
+
* capacity reservation.
|
|
61
|
+
*/
|
|
62
|
+
export declare class AdmissionLimiter {
|
|
63
|
+
#private;
|
|
64
|
+
constructor(max?: number);
|
|
65
|
+
get max(): number;
|
|
66
|
+
/** Number of admitted calls whose underlying work has not settled yet. */
|
|
67
|
+
get active(): number;
|
|
68
|
+
get saturated(): boolean;
|
|
69
|
+
/** Reserve one execution slot. Never awaits — the check and the reservation
|
|
70
|
+
* are one synchronous step, so concurrent handlers cannot both pass. */
|
|
71
|
+
tryAcquire(): boolean;
|
|
72
|
+
/** Release a previously reserved slot. Exactly one release per successful
|
|
73
|
+
* acquire; underflow is a programming error and fails loud. */
|
|
74
|
+
release(): void;
|
|
75
|
+
}
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call deadline — SPEC-PD-MCP-GATE4-001 §4.7 (Gate 4 Adapter responsibility).
|
|
3
|
+
*
|
|
4
|
+
* Every tool call gets at most `deadlineMs` (production: 10 s) counted from
|
|
5
|
+
* handler entry, covering input validation, capability call, projection and
|
|
6
|
+
* JSON serialization. The Gate 3 Capability exposes no AbortSignal, so this
|
|
7
|
+
* is HONEST non-cancellation: on expiry we stop waiting, suppress the late
|
|
8
|
+
* result (a timeout must never later be reported as success), and track the
|
|
9
|
+
* still-running operation in a bounded orphan set whose eventual settlement
|
|
10
|
+
* is observed — proving resources are released (§4.7). `Promise.race` here is
|
|
11
|
+
* explicitly NOT claimed as underlying cancellation.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DEADLINE_MS = 10_000;
|
|
14
|
+
/** Upper bound on concurrently EXECUTING capability calls (§4.7). Admission
|
|
15
|
+
* is reserved synchronously before the capability is called, so the bound is
|
|
16
|
+
* real even when every in-flight call later outlives its deadline. Small by
|
|
17
|
+
* design: a read-only recall that takes >10 s is already pathological; four
|
|
18
|
+
* concurrent ones is a stuck host, not load. */
|
|
19
|
+
export const MAX_CONCURRENT_CALLS = 4;
|
|
20
|
+
/** Upper bound on still-running timed-out operations. Orphans are a subset of
|
|
21
|
+
* admitted calls; this registry keeps their settlement observable even after
|
|
22
|
+
* the caller stopped waiting (§4.7 “限制仍在运行的超时任务数量”). */
|
|
23
|
+
export const MAX_TRACKED_ORPHANS = 4;
|
|
24
|
+
/**
|
|
25
|
+
* Run `fn` under a deadline. On timeout the returned promise settles
|
|
26
|
+
* `{ timedOut }` immediately while `fn`'s promise keeps running; it is handed
|
|
27
|
+
* to `onOrphan` (the caller registers it in the orphan limiter). If
|
|
28
|
+
* `onOrphan` returns false (limiter saturated), the caller must refuse new
|
|
29
|
+
* work — the operation is still tracked for settlement by the race itself.
|
|
30
|
+
*/
|
|
31
|
+
export async function runWithDeadline(fn, deadlineMs, onOrphan) {
|
|
32
|
+
const startedAt = Date.now();
|
|
33
|
+
let timer;
|
|
34
|
+
const timerPromise = new Promise((resolveTimer) => {
|
|
35
|
+
timer = setTimeout(() => resolveTimer(), deadlineMs);
|
|
36
|
+
// Never hold the event loop open for the deadline alone.
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
});
|
|
39
|
+
const work = fn();
|
|
40
|
+
const winner = await Promise.race([
|
|
41
|
+
work.then((value) => ({ kind: 'value', value })),
|
|
42
|
+
timerPromise.then(() => ({ kind: 'timeout' })),
|
|
43
|
+
]);
|
|
44
|
+
if (winner.kind === 'value') {
|
|
45
|
+
if (timer !== undefined)
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
// The timer cannot fire while synchronous work blocks the loop, so a value
|
|
48
|
+
// that raced in just before the deadline but finished assembling after it
|
|
49
|
+
// would otherwise be reported as an in-time success. The window covers
|
|
50
|
+
// validation → capability → projection → serialization (§4.7): past the
|
|
51
|
+
// deadline, this is a miss. The underlying promise already settled, so no
|
|
52
|
+
// orphan exists and no capacity is left reserved.
|
|
53
|
+
if (Date.now() - startedAt < deadlineMs)
|
|
54
|
+
return { ok: true, value: winner.value };
|
|
55
|
+
return { ok: false, timedOut: true };
|
|
56
|
+
}
|
|
57
|
+
if (onOrphan)
|
|
58
|
+
onOrphan(work);
|
|
59
|
+
// Observe settlement so a rejected orphan never becomes an unhandled
|
|
60
|
+
// rejection long after the caller stopped caring (resource hygiene).
|
|
61
|
+
void work.catch(() => undefined);
|
|
62
|
+
return { ok: false, timedOut: true };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bounded registry of still-running timed-out operations. `register` refuses
|
|
66
|
+
* (returns false) once `max` orphans are unsettled; settled promises remove
|
|
67
|
+
* themselves, so the limit reflects *currently running* stragglers only.
|
|
68
|
+
*/
|
|
69
|
+
export class OrphanLimiter {
|
|
70
|
+
#max;
|
|
71
|
+
#orphans = new Set();
|
|
72
|
+
constructor(max = MAX_TRACKED_ORPHANS) {
|
|
73
|
+
this.#max = max;
|
|
74
|
+
}
|
|
75
|
+
get size() {
|
|
76
|
+
return this.#orphans.size;
|
|
77
|
+
}
|
|
78
|
+
get saturated() {
|
|
79
|
+
return this.#orphans.size >= this.#max;
|
|
80
|
+
}
|
|
81
|
+
register(orphaned) {
|
|
82
|
+
if (this.saturated)
|
|
83
|
+
return false;
|
|
84
|
+
const observed = orphaned.then(() => undefined, () => undefined);
|
|
85
|
+
this.#orphans.add(observed);
|
|
86
|
+
void observed.then(() => {
|
|
87
|
+
this.#orphans.delete(observed);
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Synchronous admission semaphore for executing capability calls (§4.7).
|
|
94
|
+
*
|
|
95
|
+
* The previous design only counted already-timed-out stragglers, so N requests
|
|
96
|
+
* could all pass the saturation check before any of them timed out and then
|
|
97
|
+
* start N > limit capability calls. Admission is now reserved at handler entry
|
|
98
|
+
* — BEFORE the capability is invoked — and released only when the underlying
|
|
99
|
+
* work actually settles (normal completion, exception, or an orphaned call
|
|
100
|
+
* that outlived its deadline). A timeout therefore stops the WAIT, never the
|
|
101
|
+
* capacity reservation.
|
|
102
|
+
*/
|
|
103
|
+
export class AdmissionLimiter {
|
|
104
|
+
#max;
|
|
105
|
+
#active = 0;
|
|
106
|
+
constructor(max = MAX_CONCURRENT_CALLS) {
|
|
107
|
+
if (!Number.isInteger(max) || max < 1)
|
|
108
|
+
throw new Error('admission limit must be a positive integer');
|
|
109
|
+
this.#max = max;
|
|
110
|
+
}
|
|
111
|
+
get max() {
|
|
112
|
+
return this.#max;
|
|
113
|
+
}
|
|
114
|
+
/** Number of admitted calls whose underlying work has not settled yet. */
|
|
115
|
+
get active() {
|
|
116
|
+
return this.#active;
|
|
117
|
+
}
|
|
118
|
+
get saturated() {
|
|
119
|
+
return this.#active >= this.#max;
|
|
120
|
+
}
|
|
121
|
+
/** Reserve one execution slot. Never awaits — the check and the reservation
|
|
122
|
+
* are one synchronous step, so concurrent handlers cannot both pass. */
|
|
123
|
+
tryAcquire() {
|
|
124
|
+
if (this.saturated)
|
|
125
|
+
return false;
|
|
126
|
+
this.#active += 1;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/** Release a previously reserved slot. Exactly one release per successful
|
|
130
|
+
* acquire; underflow is a programming error and fails loud. */
|
|
131
|
+
release() {
|
|
132
|
+
if (this.#active <= 0)
|
|
133
|
+
throw new Error('admission release without a matching acquire');
|
|
134
|
+
this.#active -= 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool result envelope assembly — SPEC-PD-MCP-GATE4-001 §4.6/§4.8, obeying
|
|
3
|
+
* Gate 3 v0.5 §10.1.
|
|
4
|
+
*
|
|
5
|
+
* `isError: true` is used for exactly two families (§4.8):
|
|
6
|
+
* 1. disclosure assertion failure (the withheld capability shell already
|
|
7
|
+
* says `unavailable` + `disclosure_assertion_failed`);
|
|
8
|
+
* 2. Adapter execution failure — response_budget_exceeded,
|
|
9
|
+
* tool_execution_failed (unexpected exception / 10 s deadline /
|
|
10
|
+
* shutting down / saturated by orphaned timed-out calls).
|
|
11
|
+
* Ordinary business states (ok / no_matches / partial / not_found / plain
|
|
12
|
+
* unavailable / invalid_input) return the capability envelope verbatim with
|
|
13
|
+
* no isError marker.
|
|
14
|
+
*
|
|
15
|
+
* Adapter failure envelopes reuse the capability shell shape so the Agent
|
|
16
|
+
* always sees one `pd.capability.v1` JSON document. Messages carry a stable
|
|
17
|
+
* code + next action; they never contain stacks, paths, env values or raw
|
|
18
|
+
* business text (§4.8: no internal details to the Agent).
|
|
19
|
+
*/
|
|
20
|
+
import { type PainRecallCapabilityResult } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
21
|
+
/** MCP tool result as produced by the handler (single text content block,
|
|
22
|
+
* `structuredContent` intentionally never used — §4.6). The shapes match the
|
|
23
|
+
* SDK's expected result type (mutable content array + index signature). */
|
|
24
|
+
export interface ToolResult {
|
|
25
|
+
content: {
|
|
26
|
+
type: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
}[];
|
|
29
|
+
isError?: boolean;
|
|
30
|
+
[extra: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export declare function textResult(text: string, isError?: boolean): ToolResult;
|
|
33
|
+
/** Adapter failure envelope in the capability shell shape (§7.2 null form). */
|
|
34
|
+
export declare function adapterFailureEnvelope(code: string, message: string): PainRecallCapabilityResult;
|
|
35
|
+
/** §4.8 — does this capability envelope require `isError: true`?
|
|
36
|
+
* Only the disclosure assertion failure (the capability withheld its
|
|
37
|
+
* payload); every ordinary business status stays a normal result. */
|
|
38
|
+
export declare function requiresIsError(envelope: PainRecallCapabilityResult): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Assemble the final tool result from a capability envelope.
|
|
41
|
+
*
|
|
42
|
+
* Order: serialize → budget check → emit. Over budget returns
|
|
43
|
+
* `response_budget_exceeded` (Adapter execution failure, isError: true).
|
|
44
|
+
* The failure envelope is asserted to fit the budget itself (§4.6).
|
|
45
|
+
*/
|
|
46
|
+
export declare function assembleResult(envelope: PainRecallCapabilityResult): {
|
|
47
|
+
ok: true;
|
|
48
|
+
result: ToolResult;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
overBudgetBytes: number;
|
|
52
|
+
};
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool result envelope assembly — SPEC-PD-MCP-GATE4-001 §4.6/§4.8, obeying
|
|
3
|
+
* Gate 3 v0.5 §10.1.
|
|
4
|
+
*
|
|
5
|
+
* `isError: true` is used for exactly two families (§4.8):
|
|
6
|
+
* 1. disclosure assertion failure (the withheld capability shell already
|
|
7
|
+
* says `unavailable` + `disclosure_assertion_failed`);
|
|
8
|
+
* 2. Adapter execution failure — response_budget_exceeded,
|
|
9
|
+
* tool_execution_failed (unexpected exception / 10 s deadline /
|
|
10
|
+
* shutting down / saturated by orphaned timed-out calls).
|
|
11
|
+
* Ordinary business states (ok / no_matches / partial / not_found / plain
|
|
12
|
+
* unavailable / invalid_input) return the capability envelope verbatim with
|
|
13
|
+
* no isError marker.
|
|
14
|
+
*
|
|
15
|
+
* Adapter failure envelopes reuse the capability shell shape so the Agent
|
|
16
|
+
* always sees one `pd.capability.v1` JSON document. Messages carry a stable
|
|
17
|
+
* code + next action; they never contain stacks, paths, env values or raw
|
|
18
|
+
* business text (§4.8: no internal details to the Agent).
|
|
19
|
+
*/
|
|
20
|
+
import { CAPABILITY_SCHEMA_VERSION, OD_A_DISCLOSURE_PROFILE, RECALL_PAIN_CAPABILITY_ID, } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
21
|
+
import { withinBudget } from './budget.js';
|
|
22
|
+
export function textResult(text, isError) {
|
|
23
|
+
return isError === undefined
|
|
24
|
+
? { content: [{ type: 'text', text }] }
|
|
25
|
+
: { content: [{ type: 'text', text }], isError };
|
|
26
|
+
}
|
|
27
|
+
/** Adapter failure envelope in the capability shell shape (§7.2 null form). */
|
|
28
|
+
export function adapterFailureEnvelope(code, message) {
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: CAPABILITY_SCHEMA_VERSION,
|
|
31
|
+
capability: RECALL_PAIN_CAPABILITY_ID,
|
|
32
|
+
disclosureProfile: OD_A_DISCLOSURE_PROFILE,
|
|
33
|
+
status: 'unavailable',
|
|
34
|
+
data: null,
|
|
35
|
+
provenance: { workspaceBinding: 'configured', sources: [] },
|
|
36
|
+
warnings: [],
|
|
37
|
+
error: { code, message },
|
|
38
|
+
nextAction: {
|
|
39
|
+
code: 'retry_or_report',
|
|
40
|
+
message: 'If this repeats, run the PD host health check (pd health) and include the MCP stage in the report.',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** §4.8 — does this capability envelope require `isError: true`?
|
|
45
|
+
* Only the disclosure assertion failure (the capability withheld its
|
|
46
|
+
* payload); every ordinary business status stays a normal result. */
|
|
47
|
+
export function requiresIsError(envelope) {
|
|
48
|
+
return envelope.status === 'unavailable' && envelope.error?.code === 'disclosure_assertion_failed';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Assemble the final tool result from a capability envelope.
|
|
52
|
+
*
|
|
53
|
+
* Order: serialize → budget check → emit. Over budget returns
|
|
54
|
+
* `response_budget_exceeded` (Adapter execution failure, isError: true).
|
|
55
|
+
* The failure envelope is asserted to fit the budget itself (§4.6).
|
|
56
|
+
*/
|
|
57
|
+
export function assembleResult(envelope) {
|
|
58
|
+
const json = JSON.stringify(envelope);
|
|
59
|
+
if (withinBudget(json)) {
|
|
60
|
+
return { ok: true, result: textResult(json, requiresIsError(envelope) ? true : undefined) };
|
|
61
|
+
}
|
|
62
|
+
return { ok: false, overBudgetBytes: Buffer.byteLength(json, 'utf8') };
|
|
63
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createPdMcpServer, TOOL_NAME, toolInputSchema } from './server.js';
|
|
2
|
+
export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspace.js';
|
|
3
|
+
export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
|
|
4
|
+
export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
|
|
5
|
+
export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
|
|
6
|
+
/**
|
|
7
|
+
* Run the stdio server against the real process env. Returns the intended
|
|
8
|
+
* process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
|
|
9
|
+
*/
|
|
10
|
+
export declare function main(env?: Record<string, unknown>): Promise<number>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server process entry — SPEC-PD-MCP-GATE4-001 §5.5.
|
|
3
|
+
*
|
|
4
|
+
* Startup: resolve the fixed workspace binding (fail-closed: a missing or
|
|
5
|
+
* invalid binding logs ONE sanitized reason to stderr and exits 1 BEFORE the
|
|
6
|
+
* MCP initialize — the server never serves with an unconfirmed workspace) →
|
|
7
|
+
* build the server → connect the stdio transport → wait for requests.
|
|
8
|
+
*
|
|
9
|
+
* Shutdown (host closes stdin / terminates): stop accepting new calls
|
|
10
|
+
* (in-flight ones drain bounded by the 10 s deadline; late results are
|
|
11
|
+
* suppressed), release resources, exit 0.
|
|
12
|
+
*
|
|
13
|
+
* stdout carries MCP protocol messages ONLY (§5.3). This module never
|
|
14
|
+
* console.log's; logs go through the sanitized stderr logger.
|
|
15
|
+
*/
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { pathToFileURL } from 'node:url';
|
|
20
|
+
import { createPdMcpServer } from './server.js';
|
|
21
|
+
import { resolveWorkspaceBinding } from './workspace.js';
|
|
22
|
+
import { stderrLog } from './logging.js';
|
|
23
|
+
export { createPdMcpServer, TOOL_NAME, toolInputSchema } from './server.js';
|
|
24
|
+
export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspace.js';
|
|
25
|
+
export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
|
|
26
|
+
export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
|
|
27
|
+
export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
|
|
28
|
+
/**
|
|
29
|
+
* Run the stdio server against the real process env. Returns the intended
|
|
30
|
+
* process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
|
|
31
|
+
*/
|
|
32
|
+
export async function main(env = process.env) {
|
|
33
|
+
const binding = resolveWorkspaceBinding(env);
|
|
34
|
+
if (!binding.ok) {
|
|
35
|
+
// Fail-closed BEFORE initialize: one sanitized stderr line with the
|
|
36
|
+
// reason and the next action; nothing about values or paths.
|
|
37
|
+
stderrLog('error', 'startup_binding_failed', {
|
|
38
|
+
envVar: binding.envVar,
|
|
39
|
+
reasonCategory: 'invalid_binding',
|
|
40
|
+
reason: binding.reason,
|
|
41
|
+
nextAction: binding.nextAction,
|
|
42
|
+
});
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
// Workspace fingerprint (NOT the path): lets operators and acceptance runs
|
|
46
|
+
// prove WHICH workspace this process bound without leaking a directory
|
|
47
|
+
// (§10 forbids logging full paths). Multi-agent hosts must never see two
|
|
48
|
+
// different fingerprints for the same binding.
|
|
49
|
+
const workspaceFingerprint = createHash('sha256')
|
|
50
|
+
.update(path.resolve(binding.binding.workspaceDir))
|
|
51
|
+
.digest('hex')
|
|
52
|
+
.slice(0, 16);
|
|
53
|
+
stderrLog('info', 'startup', { host: binding.binding.host ?? 'unspecified', workspaceFingerprint });
|
|
54
|
+
const handle = createPdMcpServer({ workspaceDir: binding.binding.workspaceDir, host: binding.binding.host });
|
|
55
|
+
const transport = new StdioServerTransport();
|
|
56
|
+
await handle.mcpServer.connect(transport);
|
|
57
|
+
// Host-driven shutdown: stdin close → SDK closes the transport → we stop
|
|
58
|
+
// accepting new work, let in-flight calls drain (bounded by the deadline)
|
|
59
|
+
// and exit cleanly. The transport close callback is the single trigger.
|
|
60
|
+
transport.onclose = () => {
|
|
61
|
+
handle.beginShutdown();
|
|
62
|
+
stderrLog('info', 'transport_closed', { orphans: handle.orphans.size });
|
|
63
|
+
};
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
// Process-mode guard: only run when executed directly (not when the launcher
|
|
67
|
+
// or tests import this module). Windows-safe via pathToFileURL.
|
|
68
|
+
const { 1: invokedPath } = process.argv;
|
|
69
|
+
if (invokedPath !== undefined &&
|
|
70
|
+
import.meta.url === pathToFileURL(invokedPath).href) {
|
|
71
|
+
void main().then((code) => {
|
|
72
|
+
if (code !== 0)
|
|
73
|
+
process.exit(code);
|
|
74
|
+
// code 0: exit naturally once the event loop drains (timers unref'd).
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LogField = string | number | boolean;
|
|
2
|
+
export type LogFields = Readonly<Record<string, LogField>>;
|
|
3
|
+
export type LogLevel = 'info' | 'warn' | 'error';
|
|
4
|
+
/** Bound logger: sink fixed at construction, ≤3 params per call. */
|
|
5
|
+
export type LogFn = (level: LogLevel, event: string, fields?: LogFields) => void;
|
|
6
|
+
declare function sanitizeErrorKind(err: unknown): string;
|
|
7
|
+
/** Default stderr sink. One JSON line per event, nothing else. */
|
|
8
|
+
export declare function stderrSink(line: string): void;
|
|
9
|
+
export declare function createLogger(sink: (line: string) => void, baseFields?: LogFields): LogFn;
|
|
10
|
+
/** Convenience logger bound to stderr (startup/shutdown paths). */
|
|
11
|
+
export declare const stderrLog: LogFn;
|
|
12
|
+
export { sanitizeErrorKind };
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stderr-only structured logging — SPEC-PD-MCP-GATE4-001 §10.
|
|
3
|
+
*
|
|
4
|
+
* stdout carries MCP protocol messages ONLY; every log line goes to stderr
|
|
5
|
+
* as one JSON object. Redaction is by construction: callers may pass only
|
|
6
|
+
* scalar event/category/duration fields — never Pain text, evidence bodies,
|
|
7
|
+
* env values, raw tool results, absolute workspace/db paths or stack traces.
|
|
8
|
+
* The `errKind` field carries a sanitized exception category (the error's
|
|
9
|
+
* `name`), never `message` or `stack`.
|
|
10
|
+
*/
|
|
11
|
+
import { SERVER_NAME, SERVER_VERSION } from './version.js';
|
|
12
|
+
function sanitizeErrorKind(err) {
|
|
13
|
+
if (err !== null && typeof err === 'object' && Object.hasOwn(err, 'name') && typeof err.name === 'string') {
|
|
14
|
+
const { name } = err;
|
|
15
|
+
return name.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 60) || 'Error';
|
|
16
|
+
}
|
|
17
|
+
return 'Error';
|
|
18
|
+
}
|
|
19
|
+
/** Default stderr sink. One JSON line per event, nothing else. */
|
|
20
|
+
export function stderrSink(line) {
|
|
21
|
+
process.stderr.write(line + '\n');
|
|
22
|
+
}
|
|
23
|
+
export function createLogger(sink, baseFields) {
|
|
24
|
+
return (level, event, fields) => {
|
|
25
|
+
const entry = {
|
|
26
|
+
ts: new Date().toISOString(),
|
|
27
|
+
server: SERVER_NAME,
|
|
28
|
+
version: SERVER_VERSION,
|
|
29
|
+
level,
|
|
30
|
+
event,
|
|
31
|
+
};
|
|
32
|
+
const merged = { ...baseFields, ...fields };
|
|
33
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
34
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
35
|
+
entry[k] = v;
|
|
36
|
+
}
|
|
37
|
+
sink(JSON.stringify(entry));
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Convenience logger bound to stderr (startup/shutdown paths). */
|
|
41
|
+
export const stderrLog = createLogger(stderrSink);
|
|
42
|
+
export { sanitizeErrorKind };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server assembly — SPEC-PD-MCP-GATE4-001 §3/§4/§5 (Gate 4-B).
|
|
3
|
+
*
|
|
4
|
+
* One server implementation shared by all three hosts (Codex / OpenClaw /
|
|
5
|
+
* WorkBuddy each spawn their own process around this module). Business data
|
|
6
|
+
* flows EXCLUSIVELY through the Gate 3 Capability public subpath
|
|
7
|
+
* `@principles/core/runtime-v2/pain-recall-capability` — never the read
|
|
8
|
+
* model, never a CLI subprocess, never a second query implementation
|
|
9
|
+
* (§3.2). Core stays MCP-SDK-free (architecture guard test pins this).
|
|
10
|
+
*
|
|
11
|
+
* The single registered tool is the read-only `pd_recall_principle_pains`
|
|
12
|
+
* with an honest `readOnlyHint: true` (a host hint, not an OS boundary).
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer, type StandardSchemaWithJSON } from '@modelcontextprotocol/server';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { type PainRecallCapabilityResult } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
17
|
+
import { OrphanLimiter, AdmissionLimiter } from './deadline.js';
|
|
18
|
+
export declare const TOOL_NAME: 'pd_recall_principle_pains';
|
|
19
|
+
export declare const TOOL_DESCRIPTION = "Recall a principle's verifiably-associated historical pain experience (availability-level disclosure only, OD-A v1). Read-only.";
|
|
20
|
+
/** §4.2 input contract: `principleId` (1..200, required) and `limit`
|
|
21
|
+
* (1..10, default 3); every other property is rejected at the protocol
|
|
22
|
+
* input-validation stage (`additionalProperties: false`) before the
|
|
23
|
+
* capability is called. No path, SQL, host/session/turn/pain id, or
|
|
24
|
+
* ordering parameters exist. The capability re-validates field-by-field
|
|
25
|
+
* (defense in depth). */
|
|
26
|
+
export declare const toolInputJsonSchema: {
|
|
27
|
+
readonly type: 'object';
|
|
28
|
+
readonly properties: {
|
|
29
|
+
readonly principleId: {
|
|
30
|
+
readonly type: 'string';
|
|
31
|
+
readonly minLength: 1;
|
|
32
|
+
readonly maxLength: 200;
|
|
33
|
+
readonly description: 'Principle identifier to recall pains for.';
|
|
34
|
+
};
|
|
35
|
+
readonly limit: {
|
|
36
|
+
readonly type: 'integer';
|
|
37
|
+
readonly minimum: 1;
|
|
38
|
+
readonly maximum: 10;
|
|
39
|
+
readonly default: 3;
|
|
40
|
+
readonly description: 'Maximum number of pain entries to return (1..10, default 3).';
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
readonly required: readonly ['principleId'];
|
|
44
|
+
readonly additionalProperties: false;
|
|
45
|
+
};
|
|
46
|
+
/** Protocol-level schema handed to the SDK (JSON-Schema-first: strict
|
|
47
|
+
* rejection of undeclared parameters at the input-validation stage). */
|
|
48
|
+
export declare const toolProtocolSchema: StandardSchemaWithJSON<unknown, unknown>;
|
|
49
|
+
/** In-handler typed re-validation (idempotent post-SDK parse; applies the
|
|
50
|
+
* `limit` default and yields the typed value). Mirrors the JSON schema
|
|
51
|
+
* above — kept in lockstep by the contract tests. */
|
|
52
|
+
export declare const toolInputSchema: z.ZodObject<{
|
|
53
|
+
principleId: z.ZodString;
|
|
54
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
export type ToolInput = z.infer<typeof toolInputSchema>;
|
|
57
|
+
/** The capability entry the server calls — injectable for adapter-contract
|
|
58
|
+
* tests; production always uses the real Gate 3 Facade. */
|
|
59
|
+
export type CapabilityCall = (input: {
|
|
60
|
+
readonly workspaceDir: string;
|
|
61
|
+
readonly principleId: string;
|
|
62
|
+
readonly limit: number;
|
|
63
|
+
}) => Promise<PainRecallCapabilityResult>;
|
|
64
|
+
export interface PdMcpServerOptions {
|
|
65
|
+
readonly workspaceDir: string;
|
|
66
|
+
readonly host?: string | null;
|
|
67
|
+
readonly deadlineMs?: number;
|
|
68
|
+
readonly callCapability?: CapabilityCall;
|
|
69
|
+
readonly logSink?: (line: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface PdMcpServerHandle {
|
|
72
|
+
readonly mcpServer: McpServer;
|
|
73
|
+
/** Timeout stragglers tracked until their underlying work settles. */
|
|
74
|
+
readonly orphans: OrphanLimiter;
|
|
75
|
+
/** Executing-call capacity; admission is reserved before the capability is
|
|
76
|
+
* invoked and released only on real settlement (§4.7). */
|
|
77
|
+
readonly admissions: AdmissionLimiter;
|
|
78
|
+
/** Lifecycle flag: once closing, in-flight work drains and new calls get
|
|
79
|
+
* the shutting-down failure (§5.5). */
|
|
80
|
+
beginShutdown(): void;
|
|
81
|
+
isShuttingDown(): boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the PD MCP server (not yet connected to any transport).
|
|
85
|
+
*
|
|
86
|
+
* Handler flow per §4.7/§4.8: shutdown/saturation gates → deadline-wrapped
|
|
87
|
+
* capability call → envelope assembly (serialize → 32 KiB budget → emit) →
|
|
88
|
+
* explicit isError mapping. Nothing here writes to stdout.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createPdMcpServer(options: PdMcpServerOptions): PdMcpServerHandle;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server assembly — SPEC-PD-MCP-GATE4-001 §3/§4/§5 (Gate 4-B).
|
|
3
|
+
*
|
|
4
|
+
* One server implementation shared by all three hosts (Codex / OpenClaw /
|
|
5
|
+
* WorkBuddy each spawn their own process around this module). Business data
|
|
6
|
+
* flows EXCLUSIVELY through the Gate 3 Capability public subpath
|
|
7
|
+
* `@principles/core/runtime-v2/pain-recall-capability` — never the read
|
|
8
|
+
* model, never a CLI subprocess, never a second query implementation
|
|
9
|
+
* (§3.2). Core stays MCP-SDK-free (architecture guard test pins this).
|
|
10
|
+
*
|
|
11
|
+
* The single registered tool is the read-only `pd_recall_principle_pains`
|
|
12
|
+
* with an honest `readOnlyHint: true` (a host hint, not an OS boundary).
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { recallPrinciplePainsCapability, } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
17
|
+
import { runWithDeadline, OrphanLimiter, AdmissionLimiter, DEFAULT_DEADLINE_MS } from './deadline.js';
|
|
18
|
+
import { assembleResult, adapterFailureEnvelope, textResult } from './envelope.js';
|
|
19
|
+
import { RESPONSE_BUDGET_BYTES, withinBudget } from './budget.js';
|
|
20
|
+
import { createLogger, sanitizeErrorKind, stderrSink } from './logging.js';
|
|
21
|
+
import { SERVER_NAME, SERVER_VERSION } from './version.js';
|
|
22
|
+
export const TOOL_NAME = 'pd_recall_principle_pains';
|
|
23
|
+
export const TOOL_DESCRIPTION = 'Recall a principle\'s verifiably-associated historical pain experience (availability-level disclosure only, OD-A v1). Read-only.';
|
|
24
|
+
/** §4.2 input contract: `principleId` (1..200, required) and `limit`
|
|
25
|
+
* (1..10, default 3); every other property is rejected at the protocol
|
|
26
|
+
* input-validation stage (`additionalProperties: false`) before the
|
|
27
|
+
* capability is called. No path, SQL, host/session/turn/pain id, or
|
|
28
|
+
* ordering parameters exist. The capability re-validates field-by-field
|
|
29
|
+
* (defense in depth). */
|
|
30
|
+
export const toolInputJsonSchema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
principleId: { type: 'string', minLength: 1, maxLength: 200, description: 'Principle identifier to recall pains for.' },
|
|
34
|
+
limit: { type: 'integer', minimum: 1, maximum: 10, default: 3, description: 'Maximum number of pain entries to return (1..10, default 3).' },
|
|
35
|
+
},
|
|
36
|
+
required: ['principleId'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
};
|
|
39
|
+
/** Protocol-level schema handed to the SDK (JSON-Schema-first: strict
|
|
40
|
+
* rejection of undeclared parameters at the input-validation stage). */
|
|
41
|
+
export const toolProtocolSchema = fromJsonSchema(toolInputJsonSchema);
|
|
42
|
+
/** In-handler typed re-validation (idempotent post-SDK parse; applies the
|
|
43
|
+
* `limit` default and yields the typed value). Mirrors the JSON schema
|
|
44
|
+
* above — kept in lockstep by the contract tests. */
|
|
45
|
+
export const toolInputSchema = z.strictObject({
|
|
46
|
+
principleId: z.string().min(1).max(200),
|
|
47
|
+
limit: z.number().int().min(1).max(10).optional().default(3),
|
|
48
|
+
});
|
|
49
|
+
/** §4.8 Adapter 执行失败 row (timeout / exception / saturated / shutting
|
|
50
|
+
* down) — one sanitized `tool_execution_failed` envelope, isError: true. */
|
|
51
|
+
function executionFailedResult(message) {
|
|
52
|
+
const envelope = adapterFailureEnvelope('tool_execution_failed', message);
|
|
53
|
+
return textResult(JSON.stringify(envelope), true);
|
|
54
|
+
}
|
|
55
|
+
/** §4.8 "Server 正在关闭" row: refused without touching the capability. */
|
|
56
|
+
function shuttingDownResult() {
|
|
57
|
+
return executionFailedResult('The server is shutting down; the call was not completed.');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the PD MCP server (not yet connected to any transport).
|
|
61
|
+
*
|
|
62
|
+
* Handler flow per §4.7/§4.8: shutdown/saturation gates → deadline-wrapped
|
|
63
|
+
* capability call → envelope assembly (serialize → 32 KiB budget → emit) →
|
|
64
|
+
* explicit isError mapping. Nothing here writes to stdout.
|
|
65
|
+
*/
|
|
66
|
+
export function createPdMcpServer(options) {
|
|
67
|
+
const { workspaceDir, host = null, deadlineMs = DEFAULT_DEADLINE_MS, callCapability = recallPrinciplePainsCapability, logSink = stderrSink, } = options;
|
|
68
|
+
const orphans = new OrphanLimiter();
|
|
69
|
+
const admissions = new AdmissionLimiter();
|
|
70
|
+
let shuttingDown = false;
|
|
71
|
+
const mcpServer = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
72
|
+
const log = createLogger(logSink, { host: host ?? 'unspecified' });
|
|
73
|
+
mcpServer.registerTool(TOOL_NAME, {
|
|
74
|
+
description: TOOL_DESCRIPTION,
|
|
75
|
+
// Static type-interop shim ONLY: our zod 4.6.7 type instantiation does
|
|
76
|
+
// not match the SDK's bundled zod-4.2-based generic constraint, so the
|
|
77
|
+
// overloads fall through to the raw-shape one. Runtime validation is
|
|
78
|
+
// unaffected — the SDK still validates every call against exactly this
|
|
79
|
+
// (fromJsonSchema-built) schema. NOT a validation bypass (rc-2 scope:
|
|
80
|
+
// untrusted-data validation stays fully runtime-checked).
|
|
81
|
+
inputSchema: toolProtocolSchema,
|
|
82
|
+
annotations: { readOnlyHint: true },
|
|
83
|
+
}, async (rawInput) => {
|
|
84
|
+
const startedAt = Date.now();
|
|
85
|
+
// §4.7 admission: reserve execution capacity SYNCHRONOUSLY at handler
|
|
86
|
+
// entry, before any await and before the capability is called. Without
|
|
87
|
+
// this, N concurrent requests all pass an orphan-count check while none
|
|
88
|
+
// has timed out yet and N > limit capability calls start.
|
|
89
|
+
if (shuttingDown) {
|
|
90
|
+
log('warn', 'tool_call_refused_shutting_down', { durationMs: Date.now() - startedAt });
|
|
91
|
+
return shuttingDownResult();
|
|
92
|
+
}
|
|
93
|
+
if (!admissions.tryAcquire()) {
|
|
94
|
+
log('warn', 'tool_call_refused_saturated', {
|
|
95
|
+
durationMs: Date.now() - startedAt,
|
|
96
|
+
active: admissions.active,
|
|
97
|
+
max: admissions.max,
|
|
98
|
+
});
|
|
99
|
+
return executionFailedResult('The server is already executing the maximum number of concurrent calls; retry shortly.');
|
|
100
|
+
}
|
|
101
|
+
// The window covers validation → capability → projection →
|
|
102
|
+
// serialization; validation of the parsed input is part of the race.
|
|
103
|
+
// The `finally` releases capacity when the UNDERLYING work actually
|
|
104
|
+
// settles — a deadline stops the wait, not the reservation. An orphan
|
|
105
|
+
// therefore keeps its slot until its promise resolves/rejects.
|
|
106
|
+
const outcome = await runWithDeadline(async () => {
|
|
107
|
+
try {
|
|
108
|
+
// The SDK already validated against the zod schema; parse again
|
|
109
|
+
// for the typed value (idempotent) and count it inside the window.
|
|
110
|
+
const parsed = toolInputSchema.parse(rawInput);
|
|
111
|
+
const envelope = await callCapability({
|
|
112
|
+
workspaceDir,
|
|
113
|
+
principleId: parsed.principleId,
|
|
114
|
+
limit: parsed.limit,
|
|
115
|
+
});
|
|
116
|
+
const assembled = assembleResult(envelope);
|
|
117
|
+
if (assembled.ok)
|
|
118
|
+
return { kind: 'envelope', result: assembled.result };
|
|
119
|
+
return { kind: 'overBudget', bytes: assembled.overBudgetBytes };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// §4.8 非预期异常: map to a sanitized Adapter failure here — the
|
|
123
|
+
// raw exception (message/stack may carry paths or internals)
|
|
124
|
+
// must NEVER reach the SDK's default handler-error path.
|
|
125
|
+
return { kind: 'exception', errorKind: sanitizeErrorKind(err) };
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
admissions.release();
|
|
129
|
+
}
|
|
130
|
+
}, deadlineMs, (orphaned) => orphans.register(orphaned));
|
|
131
|
+
const durationMs = Date.now() - startedAt;
|
|
132
|
+
if (outcome.ok) {
|
|
133
|
+
switch (outcome.value.kind) {
|
|
134
|
+
case 'envelope': {
|
|
135
|
+
log('info', 'tool_call_completed', { durationMs, resultKind: 'envelope' });
|
|
136
|
+
return outcome.value.result;
|
|
137
|
+
}
|
|
138
|
+
case 'overBudget': {
|
|
139
|
+
log('warn', 'tool_call_over_budget', { durationMs, bytes: outcome.value.bytes, budget: RESPONSE_BUDGET_BYTES });
|
|
140
|
+
const failure = adapterFailureEnvelope('response_budget_exceeded', 'The response exceeded the 32 KiB MCP budget and was withheld without truncation.');
|
|
141
|
+
const failureJson = JSON.stringify(failure);
|
|
142
|
+
if (!withinBudget(failureJson)) {
|
|
143
|
+
// Cannot happen (static shell), but fail loud over silent breakage.
|
|
144
|
+
log('error', 'failure_envelope_over_budget', {});
|
|
145
|
+
}
|
|
146
|
+
return textResult(failureJson, true);
|
|
147
|
+
}
|
|
148
|
+
case 'exception': {
|
|
149
|
+
// Sanitized category only — no message, no stack, no paths.
|
|
150
|
+
log('error', 'tool_call_unexpected_exception', { durationMs, errorKind: outcome.value.errorKind });
|
|
151
|
+
return executionFailedResult('The call failed unexpectedly; the error was logged server-side with a sanitized category only.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Deadline exceeded (§4.7): stop waiting, suppress the late result,
|
|
156
|
+
// report an Adapter execution failure. Not underlying cancellation:
|
|
157
|
+
// the capacity slot stays reserved until the orphan settles.
|
|
158
|
+
log('warn', 'tool_call_deadline_exceeded', {
|
|
159
|
+
durationMs,
|
|
160
|
+
deadlineMs,
|
|
161
|
+
active: admissions.active,
|
|
162
|
+
orphans: orphans.size,
|
|
163
|
+
});
|
|
164
|
+
return executionFailedResult(`The call exceeded the ${deadlineMs} ms deadline; the late result is suppressed and will not be delivered.`);
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
mcpServer,
|
|
168
|
+
orphans,
|
|
169
|
+
admissions,
|
|
170
|
+
beginShutdown() {
|
|
171
|
+
shuttingDown = true;
|
|
172
|
+
log('info', 'shutdown_begun', { orphans: orphans.size, active: admissions.active });
|
|
173
|
+
},
|
|
174
|
+
isShuttingDown() {
|
|
175
|
+
return shuttingDown;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server identity — SPEC-PD-MCP-GATE4-001 §9.3 release manifest anchor.
|
|
3
|
+
*
|
|
4
|
+
* SERVER_VERSION mirrors the package version through the release
|
|
5
|
+
* materialization step (`syncMcpServerVersionMirror` in
|
|
6
|
+
* scripts/release/lib/version-plan.mjs); changesets alone rewrites only
|
|
7
|
+
* package.json. A unit test pins it to package.json so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SERVER_NAME: 'pd-mcp-server';
|
|
10
|
+
export declare const SERVER_VERSION: '0.2.0';
|
|
11
|
+
/** The only MCP SDK family/version this server is released against
|
|
12
|
+
* (Gate 4-A freeze, PD_MCP_GATE4A_REALITY_CHECK §3). Exact pin — no ranges. */
|
|
13
|
+
export declare const MCP_SDK_PIN: '@modelcontextprotocol/server@2.4.0';
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server identity — SPEC-PD-MCP-GATE4-001 §9.3 release manifest anchor.
|
|
3
|
+
*
|
|
4
|
+
* SERVER_VERSION mirrors the package version through the release
|
|
5
|
+
* materialization step (`syncMcpServerVersionMirror` in
|
|
6
|
+
* scripts/release/lib/version-plan.mjs); changesets alone rewrites only
|
|
7
|
+
* package.json. A unit test pins it to package.json so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export const SERVER_NAME = 'pd-mcp-server';
|
|
10
|
+
export const SERVER_VERSION = '0.2.0';
|
|
11
|
+
/** The only MCP SDK family/version this server is released against
|
|
12
|
+
* (Gate 4-A freeze, PD_MCP_GATE4A_REALITY_CHECK §3). Exact pin — no ranges. */
|
|
13
|
+
export const MCP_SDK_PIN = '@modelcontextprotocol/server@2.4.0';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const WORKSPACE_DIR_ENV: 'PD_MCP_WORKSPACE_DIR';
|
|
2
|
+
export declare const HOST_ENV: 'PD_MCP_HOST';
|
|
3
|
+
export declare const KNOWN_HOSTS: readonly string[];
|
|
4
|
+
export interface WorkspaceBinding {
|
|
5
|
+
readonly workspaceDir: string;
|
|
6
|
+
readonly host: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type WorkspaceBindingResolution = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly binding: WorkspaceBinding;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
readonly envVar: string;
|
|
15
|
+
readonly nextAction: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve and validate the launch-time workspace binding from a process env
|
|
19
|
+
* (injected for testability; production passes `process.env`). Checks, in
|
|
20
|
+
* order: presence → non-empty string → absolute path → existing directory →
|
|
21
|
+
* (optional) host marker sanity. Any failure is terminal for the process.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveWorkspaceBinding(env: Record<string, unknown>): WorkspaceBindingResolution;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup workspace binding — SPEC-PD-MCP-GATE4-001 §3.5.
|
|
3
|
+
*
|
|
4
|
+
* The business `workspaceDir` is launch configuration, NEVER a model-supplied
|
|
5
|
+
* tool parameter. It arrives via `PD_MCP_WORKSPACE_DIR`, which the host
|
|
6
|
+
* plugin's PD-owned `mcp-binding.json` (through launcher.cjs) injects.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed everywhere: a missing/relative/nonexistent binding, an unknown
|
|
9
|
+
* host marker, or any attempt to smuggle a path through the tool input
|
|
10
|
+
* refuses to serve. There is NO `process.cwd()`, `HOME` or `USERPROFILE`
|
|
11
|
+
* fallback for business workspace resolution (§3.5: no implicit fallback).
|
|
12
|
+
*
|
|
13
|
+
* This module reads ONLY install/startup metadata (one env + one stat);
|
|
14
|
+
* business data stays behind the Gate 3 Capability.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
export const WORKSPACE_DIR_ENV = 'PD_MCP_WORKSPACE_DIR';
|
|
19
|
+
export const HOST_ENV = 'PD_MCP_HOST';
|
|
20
|
+
export const KNOWN_HOSTS = ['codex', 'openclaw', 'workbuddy'];
|
|
21
|
+
/**
|
|
22
|
+
* Resolve and validate the launch-time workspace binding from a process env
|
|
23
|
+
* (injected for testability; production passes `process.env`). Checks, in
|
|
24
|
+
* order: presence → non-empty string → absolute path → existing directory →
|
|
25
|
+
* (optional) host marker sanity. Any failure is terminal for the process.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveWorkspaceBinding(env) {
|
|
28
|
+
const raw = env[WORKSPACE_DIR_ENV];
|
|
29
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: 'workspace binding env is missing or empty',
|
|
33
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
34
|
+
nextAction: `Re-run the PD host setup (this writes the PD-owned mcp-binding.json and launches via launcher.cjs). Do not set ${WORKSPACE_DIR_ENV} by hand from agent context.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!path.isAbsolute(raw)) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: 'workspace binding is not an absolute path',
|
|
41
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
42
|
+
nextAction: 'Re-run the PD host setup so the binding records the absolute governance workspace directory.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = fs.statSync(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'workspace binding directory does not exist or is not readable',
|
|
53
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
54
|
+
nextAction: 'Re-run the PD host setup; if the workspace was moved, re-install PD for this host so the binding is regenerated.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!stat.isDirectory()) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
reason: 'workspace binding is not a directory',
|
|
61
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
62
|
+
nextAction: 'Re-run the PD host setup so the binding points at the governance workspace directory.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const host = env[HOST_ENV];
|
|
66
|
+
if (host !== undefined) {
|
|
67
|
+
if (typeof host !== 'string' || !KNOWN_HOSTS.includes(host)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: 'host marker is not one of the known PD hosts',
|
|
71
|
+
envVar: HOST_ENV,
|
|
72
|
+
nextAction: 'Re-run the PD host setup; the host marker is written by the PD installer and must not be edited.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, binding: { workspaceDir: path.resolve(raw), host: typeof host === 'string' ? host : null } };
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/pd-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Principles Disciple MCP Server — the single shared MCP server implementation for the three PD hosts (Codex, OpenClaw, WorkBuddy). Read-only pain recall through the Gate 3 Capability (SPEC-PD-MCP-GATE4-001).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"NOTICE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node ../../scripts/build/clean-dist.mjs",
|
|
23
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node ../../scripts/build/check-dist-hygiene.mjs",
|
|
24
|
+
"test": "npm run build && vitest run",
|
|
25
|
+
"test:coverage": "vitest run --coverage",
|
|
26
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
30
|
+
"@principles/core": "^2.0.0",
|
|
31
|
+
"zod": "4.6.7"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
35
|
+
"@types/node": "^26.4.1",
|
|
36
|
+
"typescript": "^7.0.2",
|
|
37
|
+
"vite": "^8.3.4",
|
|
38
|
+
"vitest": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/csuzngjh/principles.git",
|
|
44
|
+
"directory": "packages/pd-mcp-server"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"gitHead": "d3355465b090daf098581cdb4c174634fe1e5279"
|
|
50
|
+
}
|