pi-session-mcp 0.0.0-stage → 0.6.2
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/.pi-session-mcp-cli/config.js +88 -0
- package/.pi-session-mcp-cli/doctor-cli.js +11 -0
- package/.pi-session-mcp-cli/doctor.js +178 -0
- package/.pi-session-mcp-cli/execution-profile.js +29 -0
- package/.pi-session-mcp-cli/mcp-config.js +70 -0
- package/.pi-session-mcp-cli/package-metadata.js +32 -0
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +57 -0
- package/LICENSE +21 -0
- package/README.md +183 -2
- package/SECURITY.md +37 -0
- package/dist/capabilities.d.ts +25 -0
- package/dist/capabilities.js +26 -0
- package/dist/client-adapters/claude-code.d.ts +4 -0
- package/dist/client-adapters/claude-code.js +33 -0
- package/dist/client-adapters/codex.d.ts +4 -0
- package/dist/client-adapters/codex.js +73 -0
- package/dist/client-adapters/command.d.ts +12 -0
- package/dist/client-adapters/command.js +25 -0
- package/dist/client-adapters/index.d.ts +4 -0
- package/dist/client-adapters/index.js +3 -0
- package/dist/client-adapters/mcp-verifier.d.ts +20 -0
- package/dist/client-adapters/mcp-verifier.js +174 -0
- package/dist/client-adapters/types.d.ts +54 -0
- package/dist/client-adapters/types.js +1 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +88 -0
- package/dist/doctor-cli.d.ts +1 -0
- package/dist/doctor-cli.js +11 -0
- package/dist/doctor.d.ts +47 -0
- package/dist/doctor.js +178 -0
- package/dist/entry-point.d.ts +5 -0
- package/dist/entry-point.js +16 -0
- package/dist/execution-profile.d.ts +37 -0
- package/dist/execution-profile.js +29 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +51 -0
- package/dist/mcp-config.d.ts +22 -0
- package/dist/mcp-config.js +70 -0
- package/dist/mcp-session-tools.d.ts +54 -0
- package/dist/mcp-session-tools.js +323 -0
- package/dist/mcp-stdio-client.d.ts +3 -0
- package/dist/mcp-stdio-client.js +59 -0
- package/dist/package-metadata.d.ts +18 -0
- package/dist/package-metadata.js +32 -0
- package/dist/pi-adapter.d.ts +42 -0
- package/dist/pi-adapter.js +22 -0
- package/dist/process-diagnostics.d.ts +8 -0
- package/dist/process-diagnostics.js +9 -0
- package/dist/sdk-pi-adapter.d.ts +75 -0
- package/dist/sdk-pi-adapter.js +410 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +102 -0
- package/dist/session-registry.d.ts +104 -0
- package/dist/session-registry.js +373 -0
- package/dist/setup-cli.d.ts +11 -0
- package/dist/setup-cli.js +38 -0
- package/dist/setup-command-guardian.d.ts +2 -0
- package/dist/setup-command-guardian.js +297 -0
- package/dist/setup-main.d.ts +8 -0
- package/dist/setup-main.js +60 -0
- package/dist/setup-ownership.d.ts +53 -0
- package/dist/setup-ownership.js +228 -0
- package/dist/setup-process.d.ts +30 -0
- package/dist/setup-process.js +385 -0
- package/dist/setup-result.d.ts +22 -0
- package/dist/setup-result.js +8 -0
- package/dist/setup.d.ts +55 -0
- package/dist/setup.js +327 -0
- package/dist/shutdown.d.ts +10 -0
- package/dist/shutdown.js +52 -0
- package/docs/acceptance.md +227 -0
- package/docs/adr/0001-stdio-first.md +12 -0
- package/docs/adr/0002-embedded-pi-sdk.md +11 -0
- package/docs/adr/0003-post-mvp-maintenance.md +60 -0
- package/docs/adr/0004-client-contract-not-version.md +31 -0
- package/docs/architecture.md +82 -0
- package/docs/client-setup-design.md +90 -0
- package/docs/client-setup.md +207 -0
- package/docs/doctor-design.md +47 -0
- package/docs/installation.md +352 -0
- package/docs/releasing.md +216 -0
- package/docs/session-mcp.md +260 -0
- package/docs/threat-model.md +89 -0
- package/docs/tool-contracts.md +143 -0
- package/package.json +64 -4
- package/pi-session-mcp.example.json +21 -0
- package/scripts/doctor.mjs +72 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type AgentSessionEvent, type CreateAgentSessionOptions, type CreateAgentSessionResult, DefaultResourceLoader, ModelRuntime, type PromptOptions, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { PermissionProfile, ResolvedExecutionProfile, ThinkingLevel } from "./execution-profile.js";
|
|
3
|
+
import { type McpConnectionFactory } from "./mcp-session-tools.js";
|
|
4
|
+
import { type PiSessionAdapter, type PiSessionCreationErrorCode, type PiSessionHandle, type PiTurnOutcome } from "./pi-adapter.js";
|
|
5
|
+
type CreateSession = (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
|
|
6
|
+
export declare const AUTH_OPERATION_TIMEOUT_MS = 5000;
|
|
7
|
+
/** Server-side bound for SDK session construction. The SDK does not document cancellation. */
|
|
8
|
+
export declare const SESSION_CREATION_TIMEOUT_MS = 10000;
|
|
9
|
+
export interface SdkPiAdapterDiagnostic {
|
|
10
|
+
readonly level: "error";
|
|
11
|
+
readonly event: "pi_session_creation_failed";
|
|
12
|
+
readonly stage: "runtime" | "provider" | "model" | "check_auth" | "get_auth" | "availability" | "resource_loader" | "mcp_tools" | "create_session" | "postcheck";
|
|
13
|
+
readonly code: PiSessionCreationErrorCode;
|
|
14
|
+
readonly executionProfile: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
readonly model: string;
|
|
17
|
+
readonly thinkingLevel: ThinkingLevel;
|
|
18
|
+
readonly serverAlias?: string;
|
|
19
|
+
readonly toolAlias?: string;
|
|
20
|
+
readonly discoveredToolCount?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SdkPiSessionAdapterOptions {
|
|
23
|
+
readonly modelRuntimeFactory?: () => Promise<ModelRuntime>;
|
|
24
|
+
readonly createSession?: CreateSession;
|
|
25
|
+
readonly diagnosticSink?: (diagnostic: SdkPiAdapterDiagnostic) => void;
|
|
26
|
+
readonly mcpConnectionFactory?: McpConnectionFactory;
|
|
27
|
+
}
|
|
28
|
+
type SdkAssistantMessage = Extract<Extract<AgentSessionEvent, {
|
|
29
|
+
type: "message_end";
|
|
30
|
+
}>["message"], {
|
|
31
|
+
role: "assistant";
|
|
32
|
+
}>;
|
|
33
|
+
/** Derived from the root-exported `AgentSessionEvent`; the SDK root does not export `StopReason`. */
|
|
34
|
+
export type SdkStopReason = SdkAssistantMessage["stopReason"];
|
|
35
|
+
/**
|
|
36
|
+
* Projects the final assistant message of a prompt onto a turn outcome.
|
|
37
|
+
*
|
|
38
|
+
* The switch is exhaustive on purpose: a stop reason added by a future SDK fails the
|
|
39
|
+
* typecheck (and therefore the Pi SDK canary) instead of being mapped silently. Runtime
|
|
40
|
+
* values outside the SDK union, a missing stop reason included, fail closed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeAssistantOutcome(message: unknown): PiTurnOutcome;
|
|
43
|
+
export declare const PROFILE_TOOLS: Readonly<Record<PermissionProfile, readonly string[]>>;
|
|
44
|
+
/** Derived from the root-exported `PromptOptions`; the SDK does not export the union itself. */
|
|
45
|
+
export type PromptDisposition = Parameters<NonNullable<PromptOptions["preflightResult"]>>[0];
|
|
46
|
+
/**
|
|
47
|
+
* Whether an SDK prompt disposition admits a public turn. The SDK reports how an accepted
|
|
48
|
+
* prompt was dispatched and does not call the hook on rejection: `prompt()` rejects instead,
|
|
49
|
+
* which the registry already projects as `prompt_rejected`.
|
|
50
|
+
*
|
|
51
|
+
* The switch is exhaustive on purpose: a disposition added by a future SDK fails the
|
|
52
|
+
* typecheck (and therefore the Pi SDK canary) instead of being mapped silently.
|
|
53
|
+
*/
|
|
54
|
+
export declare function admitsTurn(disposition: PromptDisposition): boolean;
|
|
55
|
+
export declare function createPhase0ResourceLoader(cwd: string): Promise<{
|
|
56
|
+
resourceLoader: DefaultResourceLoader;
|
|
57
|
+
settingsManager: SettingsManager;
|
|
58
|
+
}>;
|
|
59
|
+
export declare class SdkPiSessionAdapter implements PiSessionAdapter {
|
|
60
|
+
private runtimePromise;
|
|
61
|
+
private readonly modelRuntimeFactory;
|
|
62
|
+
private readonly createSession;
|
|
63
|
+
private readonly diagnosticSink;
|
|
64
|
+
private readonly mcpConnectionFactory;
|
|
65
|
+
constructor(options?: SdkPiSessionAdapterOptions);
|
|
66
|
+
create(input: {
|
|
67
|
+
cwd: string;
|
|
68
|
+
executionProfile: ResolvedExecutionProfile;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}): Promise<PiSessionHandle>;
|
|
71
|
+
private createSessionWithDeadline;
|
|
72
|
+
private getRuntime;
|
|
73
|
+
private failure;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { classifyMcpStartupError, connectSessionMcpTools, } from "./mcp-session-tools.js";
|
|
3
|
+
import { createStdioMcpConnection } from "./mcp-stdio-client.js";
|
|
4
|
+
import { PiSessionCreationError, } from "./pi-adapter.js";
|
|
5
|
+
export const AUTH_OPERATION_TIMEOUT_MS = 5_000;
|
|
6
|
+
/** Server-side bound for SDK session construction. The SDK does not document cancellation. */
|
|
7
|
+
export const SESSION_CREATION_TIMEOUT_MS = 10_000;
|
|
8
|
+
class AuthDeadlineError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("authentication operation timed out");
|
|
11
|
+
this.name = "AuthDeadlineError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class SessionCreationDeadlineError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super("Pi session creation timed out");
|
|
17
|
+
this.name = "SessionCreationDeadlineError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function disposeSdkSession(session) {
|
|
21
|
+
try {
|
|
22
|
+
session.dispose();
|
|
23
|
+
}
|
|
24
|
+
catch { /* SDK cleanup is best effort and never exposes internals. */ }
|
|
25
|
+
}
|
|
26
|
+
async function withAuthDeadline(operation) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
let timeout;
|
|
29
|
+
const operationPromise = Promise.resolve().then(() => operation(controller.signal));
|
|
30
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
31
|
+
timeout = setTimeout(() => {
|
|
32
|
+
controller.abort();
|
|
33
|
+
reject(new AuthDeadlineError());
|
|
34
|
+
}, AUTH_OPERATION_TIMEOUT_MS);
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
return await Promise.race([operationPromise, timeoutPromise]);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (timeout)
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
controller.abort();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Projects the final assistant message of a prompt onto a turn outcome.
|
|
47
|
+
*
|
|
48
|
+
* The switch is exhaustive on purpose: a stop reason added by a future SDK fails the
|
|
49
|
+
* typecheck (and therefore the Pi SDK canary) instead of being mapped silently. Runtime
|
|
50
|
+
* values outside the SDK union, a missing stop reason included, fail closed.
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeAssistantOutcome(message) {
|
|
53
|
+
if (!isAssistantMessage(message))
|
|
54
|
+
return { status: "failed" };
|
|
55
|
+
const stopReason = message.stopReason;
|
|
56
|
+
switch (stopReason) {
|
|
57
|
+
case "stop":
|
|
58
|
+
// Cut off by the output limit; the truncated text is still the turn's answer.
|
|
59
|
+
case "length":
|
|
60
|
+
// The agent loop ends on a tool call only when the tool batch terminated the run.
|
|
61
|
+
case "toolUse":
|
|
62
|
+
return { status: "completed", assistantText: message.content.filter(isTextBlock).map((block) => block.text).join("") };
|
|
63
|
+
case "aborted":
|
|
64
|
+
return { status: "aborted" };
|
|
65
|
+
case "error":
|
|
66
|
+
// Not a finished answer: `pending` marks a message still streaming, `deferred` a
|
|
67
|
+
// response the provider has not delivered yet.
|
|
68
|
+
case "pending":
|
|
69
|
+
case "deferred":
|
|
70
|
+
return { status: "failed" };
|
|
71
|
+
default: {
|
|
72
|
+
const unhandled = stopReason;
|
|
73
|
+
void unhandled;
|
|
74
|
+
return { status: "failed" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export const PROFILE_TOOLS = {
|
|
79
|
+
"read-only": ["read", "grep", "find", "ls"],
|
|
80
|
+
coding: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Whether an SDK prompt disposition admits a public turn. The SDK reports how an accepted
|
|
84
|
+
* prompt was dispatched and does not call the hook on rejection: `prompt()` rejects instead,
|
|
85
|
+
* which the registry already projects as `prompt_rejected`.
|
|
86
|
+
*
|
|
87
|
+
* The switch is exhaustive on purpose: a disposition added by a future SDK fails the
|
|
88
|
+
* typecheck (and therefore the Pi SDK canary) instead of being mapped silently.
|
|
89
|
+
*/
|
|
90
|
+
export function admitsTurn(disposition) {
|
|
91
|
+
switch (disposition) {
|
|
92
|
+
case "started":
|
|
93
|
+
return true;
|
|
94
|
+
// Only while streaming with `streamingBehavior`; Pi Session MCP never passes it and serializes turns.
|
|
95
|
+
case "queued":
|
|
96
|
+
// An extension command or input handler consumed the prompt without a model turn. The
|
|
97
|
+
// isolated resource loader (`noExtensions`) makes this unreachable; no public turn exists.
|
|
98
|
+
case "handled":
|
|
99
|
+
return false;
|
|
100
|
+
default: {
|
|
101
|
+
const unhandled = disposition;
|
|
102
|
+
void unhandled;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
class SdkHandle {
|
|
108
|
+
session;
|
|
109
|
+
appliedSelection;
|
|
110
|
+
mcpTools;
|
|
111
|
+
disposed = false;
|
|
112
|
+
disposal;
|
|
113
|
+
subscriptions = new Set();
|
|
114
|
+
constructor(session, appliedSelection, mcpTools) {
|
|
115
|
+
this.session = session;
|
|
116
|
+
this.appliedSelection = appliedSelection;
|
|
117
|
+
this.mcpTools = mcpTools;
|
|
118
|
+
}
|
|
119
|
+
get sdkSessionId() { return this.session.sessionId; }
|
|
120
|
+
prompt(text, preflight, onActivity) {
|
|
121
|
+
if (this.disposed)
|
|
122
|
+
return Promise.resolve({ status: "failed" });
|
|
123
|
+
let finalAssistant;
|
|
124
|
+
/** Observer failures must never disturb SDK event handling or the turn outcome. */
|
|
125
|
+
const reportActivity = () => {
|
|
126
|
+
if (!onActivity)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
onActivity();
|
|
130
|
+
}
|
|
131
|
+
catch { /* Liveness reporting is best effort and content-free. */ }
|
|
132
|
+
};
|
|
133
|
+
const unsubscribe = this.session.subscribe((event) => {
|
|
134
|
+
if (event.type === "tool_execution_end")
|
|
135
|
+
reportActivity();
|
|
136
|
+
else if (event.type === "message_end" && isAssistantMessage(event.message)) {
|
|
137
|
+
finalAssistant = event.message;
|
|
138
|
+
reportActivity();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
let preflightSeen = false;
|
|
142
|
+
let cleaned = false;
|
|
143
|
+
const signalPreflight = (accepted) => {
|
|
144
|
+
if (preflightSeen || this.disposed)
|
|
145
|
+
return;
|
|
146
|
+
preflightSeen = true;
|
|
147
|
+
preflight(accepted);
|
|
148
|
+
};
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
if (cleaned)
|
|
151
|
+
return;
|
|
152
|
+
cleaned = true;
|
|
153
|
+
this.subscriptions.delete(cleanup);
|
|
154
|
+
try {
|
|
155
|
+
unsubscribe();
|
|
156
|
+
}
|
|
157
|
+
catch { /* subscription cleanup is best effort */ }
|
|
158
|
+
};
|
|
159
|
+
this.subscriptions.add(cleanup);
|
|
160
|
+
let promptResult;
|
|
161
|
+
try {
|
|
162
|
+
promptResult = this.session.prompt(text, { preflightResult: (disposition) => signalPreflight(admitsTurn(disposition)) });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
cleanup();
|
|
166
|
+
throw new Error("Pi prompt failed");
|
|
167
|
+
}
|
|
168
|
+
return promptResult.then(() => (this.disposed ? { status: "failed" } : normalizeAssistantOutcome(finalAssistant)), () => ({ status: "failed" })).finally(cleanup);
|
|
169
|
+
}
|
|
170
|
+
abort() { return this.session.abort(); }
|
|
171
|
+
dispose() {
|
|
172
|
+
if (this.disposal)
|
|
173
|
+
return this.disposal;
|
|
174
|
+
this.disposed = true;
|
|
175
|
+
let finish;
|
|
176
|
+
// Publish the shared promise before synchronous cleanup can re-enter dispose().
|
|
177
|
+
this.disposal = new Promise((resolve) => { finish = resolve; });
|
|
178
|
+
const closed = this.mcpTools.close();
|
|
179
|
+
for (const cleanup of [...this.subscriptions])
|
|
180
|
+
cleanup();
|
|
181
|
+
disposeSdkSession(this.session);
|
|
182
|
+
void closed.then(finish, finish);
|
|
183
|
+
return this.disposal;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isAssistantMessage(message) {
|
|
187
|
+
if (!message || typeof message !== "object")
|
|
188
|
+
return false;
|
|
189
|
+
const candidate = message;
|
|
190
|
+
return candidate.role === "assistant" && Array.isArray(candidate.content);
|
|
191
|
+
}
|
|
192
|
+
function isTextBlock(block) {
|
|
193
|
+
if (!block || typeof block !== "object")
|
|
194
|
+
return false;
|
|
195
|
+
const candidate = block;
|
|
196
|
+
return candidate.type === "text" && typeof candidate.text === "string";
|
|
197
|
+
}
|
|
198
|
+
export async function createPhase0ResourceLoader(cwd) {
|
|
199
|
+
const settingsManager = SettingsManager.inMemory(undefined, { projectTrusted: false });
|
|
200
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
201
|
+
cwd,
|
|
202
|
+
agentDir: cwd,
|
|
203
|
+
settingsManager,
|
|
204
|
+
noExtensions: true,
|
|
205
|
+
noSkills: true,
|
|
206
|
+
noPromptTemplates: true,
|
|
207
|
+
noThemes: true,
|
|
208
|
+
noContextFiles: false,
|
|
209
|
+
systemPrompt: "phase-0-system-prompt-disabled",
|
|
210
|
+
appendSystemPrompt: [],
|
|
211
|
+
systemPromptOverride: () => undefined,
|
|
212
|
+
appendSystemPromptOverride: () => [],
|
|
213
|
+
});
|
|
214
|
+
await resourceLoader.reload();
|
|
215
|
+
return { resourceLoader, settingsManager };
|
|
216
|
+
}
|
|
217
|
+
export class SdkPiSessionAdapter {
|
|
218
|
+
runtimePromise;
|
|
219
|
+
modelRuntimeFactory;
|
|
220
|
+
createSession;
|
|
221
|
+
diagnosticSink;
|
|
222
|
+
mcpConnectionFactory;
|
|
223
|
+
constructor(options = {}) {
|
|
224
|
+
this.modelRuntimeFactory = options.modelRuntimeFactory ?? (() => ModelRuntime.create({ refreshOnCreate: false }));
|
|
225
|
+
this.createSession = options.createSession ?? createAgentSession;
|
|
226
|
+
this.diagnosticSink = options.diagnosticSink ?? (() => undefined);
|
|
227
|
+
this.mcpConnectionFactory = options.mcpConnectionFactory ?? createStdioMcpConnection;
|
|
228
|
+
}
|
|
229
|
+
async create(input) {
|
|
230
|
+
const { executionProfile } = input;
|
|
231
|
+
let runtime;
|
|
232
|
+
try {
|
|
233
|
+
runtime = await this.getRuntime();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw this.failure(executionProfile, "runtime", "pi_session_creation_failed");
|
|
237
|
+
}
|
|
238
|
+
let provider;
|
|
239
|
+
try {
|
|
240
|
+
provider = runtime.getProvider(executionProfile.provider);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw this.failure(executionProfile, "provider", "pi_session_creation_failed");
|
|
244
|
+
}
|
|
245
|
+
if (!provider)
|
|
246
|
+
throw this.failure(executionProfile, "provider", "unknown_provider");
|
|
247
|
+
let model;
|
|
248
|
+
try {
|
|
249
|
+
model = runtime.getModel(executionProfile.provider, executionProfile.model);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
throw this.failure(executionProfile, "model", "pi_session_creation_failed");
|
|
253
|
+
}
|
|
254
|
+
if (!model)
|
|
255
|
+
throw this.failure(executionProfile, "model", "unknown_model");
|
|
256
|
+
let authenticated;
|
|
257
|
+
try {
|
|
258
|
+
authenticated = Boolean(await withAuthDeadline((signal) => runtime.checkAuth(executionProfile.provider, { signal })));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw this.failure(executionProfile, "check_auth", "local_authentication_unavailable");
|
|
262
|
+
}
|
|
263
|
+
if (!authenticated)
|
|
264
|
+
throw this.failure(executionProfile, "check_auth", "local_authentication_missing");
|
|
265
|
+
try {
|
|
266
|
+
// AuthResult can contain credentials, so reduce it immediately.
|
|
267
|
+
authenticated = Boolean(await withAuthDeadline((signal) => runtime.getAuth(model, { signal })));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
throw this.failure(executionProfile, "get_auth", "local_authentication_unavailable");
|
|
271
|
+
}
|
|
272
|
+
if (!authenticated)
|
|
273
|
+
throw this.failure(executionProfile, "get_auth", "local_authentication_missing");
|
|
274
|
+
let available;
|
|
275
|
+
try {
|
|
276
|
+
available = await withAuthDeadline((signal) => runtime.getAvailable(executionProfile.provider, { signal }));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
throw this.failure(executionProfile, "availability", "pi_session_creation_failed");
|
|
280
|
+
}
|
|
281
|
+
if (!available.some((candidate) => candidate.provider === executionProfile.provider && candidate.id === executionProfile.model)) {
|
|
282
|
+
throw this.failure(executionProfile, "availability", "model_unavailable");
|
|
283
|
+
}
|
|
284
|
+
let resourceLoader;
|
|
285
|
+
let settingsManager;
|
|
286
|
+
try {
|
|
287
|
+
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw this.failure(executionProfile, "resource_loader", "pi_session_creation_failed");
|
|
291
|
+
}
|
|
292
|
+
let mcpTools;
|
|
293
|
+
try {
|
|
294
|
+
mcpTools = await connectSessionMcpTools(executionProfile.mcpServers ?? {}, executionProfile.permissionProfile, input.cwd, this.mcpConnectionFactory, input.signal === undefined ? {} : { signal: input.signal });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const failure = classifyMcpStartupError(error);
|
|
298
|
+
throw this.failure(executionProfile, "mcp_tools", failure?.code ?? "external_mcp_unavailable", failure);
|
|
299
|
+
}
|
|
300
|
+
const tools = [...PROFILE_TOOLS[executionProfile.permissionProfile], ...mcpTools.tools.map((tool) => tool.name)];
|
|
301
|
+
let session;
|
|
302
|
+
try {
|
|
303
|
+
({ session } = await this.createSessionWithDeadline({
|
|
304
|
+
cwd: input.cwd,
|
|
305
|
+
modelRuntime: runtime,
|
|
306
|
+
model,
|
|
307
|
+
thinkingLevel: executionProfile.thinkingLevel,
|
|
308
|
+
sessionManager: SessionManager.inMemory(input.cwd),
|
|
309
|
+
settingsManager,
|
|
310
|
+
resourceLoader,
|
|
311
|
+
// With an explicit tools list, Pi also needs custom tool names here to activate them.
|
|
312
|
+
tools,
|
|
313
|
+
...(mcpTools.tools.length === 0 ? {} : { customTools: mcpTools.tools }),
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
await mcpTools.close();
|
|
318
|
+
throw this.failure(executionProfile, "create_session", "pi_session_creation_failed");
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
if (session.model?.provider !== executionProfile.provider || session.model.id !== executionProfile.model) {
|
|
322
|
+
throw this.failure(executionProfile, "postcheck", "execution_selection_mismatch");
|
|
323
|
+
}
|
|
324
|
+
if (session.thinkingLevel !== executionProfile.thinkingLevel) {
|
|
325
|
+
throw this.failure(executionProfile, "postcheck", "thinking_level_unavailable");
|
|
326
|
+
}
|
|
327
|
+
if (mcpTools.tools.length > 0) {
|
|
328
|
+
const active = new Set(session.getActiveToolNames());
|
|
329
|
+
if (active.size !== tools.length || tools.some((name) => !active.has(name))) {
|
|
330
|
+
throw this.failure(executionProfile, "postcheck", "external_mcp_activation_mismatch");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return new SdkHandle(session, {
|
|
334
|
+
provider: session.model.provider,
|
|
335
|
+
model: session.model.id,
|
|
336
|
+
thinkingLevel: session.thinkingLevel,
|
|
337
|
+
}, mcpTools);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
disposeSdkSession(session);
|
|
341
|
+
await mcpTools.close();
|
|
342
|
+
if (error instanceof PiSessionCreationError)
|
|
343
|
+
throw error;
|
|
344
|
+
throw this.failure(executionProfile, "postcheck", "pi_session_creation_failed");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async createSessionWithDeadline(options) {
|
|
348
|
+
let creation;
|
|
349
|
+
try {
|
|
350
|
+
creation = Promise.resolve(this.createSession(options));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
throw new SessionCreationDeadlineError();
|
|
354
|
+
}
|
|
355
|
+
let timedOut = false;
|
|
356
|
+
// This continuation is installed immediately and owns late cleanup. Its rejection
|
|
357
|
+
// branch is required even when the deadline wins the race.
|
|
358
|
+
const observed = creation.then((result) => {
|
|
359
|
+
if (timedOut)
|
|
360
|
+
disposeSdkSession(result.session);
|
|
361
|
+
return result;
|
|
362
|
+
}, (error) => { throw error; });
|
|
363
|
+
let timer;
|
|
364
|
+
const timeout = new Promise((_, reject) => {
|
|
365
|
+
timer = setTimeout(() => {
|
|
366
|
+
timedOut = true;
|
|
367
|
+
reject(new SessionCreationDeadlineError());
|
|
368
|
+
}, SESSION_CREATION_TIMEOUT_MS);
|
|
369
|
+
});
|
|
370
|
+
try {
|
|
371
|
+
return await Promise.race([observed, timeout]);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
if (timer)
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
getRuntime() {
|
|
379
|
+
if (!this.runtimePromise) {
|
|
380
|
+
const pending = this.modelRuntimeFactory();
|
|
381
|
+
this.runtimePromise = pending;
|
|
382
|
+
void pending.catch(() => {
|
|
383
|
+
if (this.runtimePromise === pending)
|
|
384
|
+
this.runtimePromise = undefined;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return this.runtimePromise;
|
|
388
|
+
}
|
|
389
|
+
failure(executionProfile, stage, code, metadata) {
|
|
390
|
+
try {
|
|
391
|
+
this.diagnosticSink({
|
|
392
|
+
level: "error",
|
|
393
|
+
event: "pi_session_creation_failed",
|
|
394
|
+
stage,
|
|
395
|
+
code,
|
|
396
|
+
executionProfile: executionProfile.alias,
|
|
397
|
+
provider: executionProfile.provider,
|
|
398
|
+
model: executionProfile.model,
|
|
399
|
+
thinkingLevel: executionProfile.thinkingLevel,
|
|
400
|
+
...(metadata?.serverAlias === undefined ? {} : { serverAlias: metadata.serverAlias }),
|
|
401
|
+
...(metadata?.toolAlias === undefined ? {} : { toolAlias: metadata.toolAlias }),
|
|
402
|
+
...(metadata?.discoveredToolCount === undefined ? {} : { discoveredToolCount: metadata.discoveredToolCount }),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Diagnostics are never allowed to influence session safety.
|
|
407
|
+
}
|
|
408
|
+
return new PiSessionCreationError(code);
|
|
409
|
+
}
|
|
410
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { type ConfigurationMetadata } from "./config.js";
|
|
3
|
+
import { type ConfiguredExecutionProfile } from "./execution-profile.js";
|
|
4
|
+
import { type SessionRegistry } from "./session-registry.js";
|
|
5
|
+
export declare function createServer(registry: SessionRegistry, workspaces: ReadonlyMap<string, string>, executionProfiles: ReadonlyMap<string, ConfiguredExecutionProfile>, defaultExecutionProfile: string, configuration: ConfigurationMetadata): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import packageMetadata from "../package.json" with { type: "json" };
|
|
4
|
+
import { projectCapabilities } from "./capabilities.js";
|
|
5
|
+
import { CONFIGURATION_FINGERPRINT_PATTERN, RELOAD_POLICY } from "./config.js";
|
|
6
|
+
import { PiSessionCreationError } from "./pi-adapter.js";
|
|
7
|
+
import { ALIAS_PATTERN, toResolvedExecutionProfile } from "./execution-profile.js";
|
|
8
|
+
import { RegistryError } from "./session-registry.js";
|
|
9
|
+
const thinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
10
|
+
const viewSchema = z.object({
|
|
11
|
+
id: z.string(), workspace: z.string(), executionProfile: z.string(), profile: z.enum(["read-only", "coding"]),
|
|
12
|
+
provider: z.string(), model: z.string(), thinkingLevel: thinkingLevelSchema,
|
|
13
|
+
state: z.enum(["idle", "running", "failed", "closing", "closed"]), createdAt: z.string(), updatedAt: z.string(),
|
|
14
|
+
lastError: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
const aliasSchema = z.string().regex(ALIAS_PATTERN);
|
|
17
|
+
const successSchema = z.object({ ok: z.literal(true), session: viewSchema });
|
|
18
|
+
const turnBase = { turnId: z.string().uuid(), sessionId: z.string().uuid(), startedAt: z.string(), updatedAt: z.string() };
|
|
19
|
+
const turnSchema = z.discriminatedUnion("state", [
|
|
20
|
+
z.object({ ...turnBase, state: z.literal("running") }),
|
|
21
|
+
z.object({ ...turnBase, state: z.literal("completed"), completedAt: z.string(), assistantText: z.string(), truncated: z.boolean() }),
|
|
22
|
+
z.object({ ...turnBase, state: z.literal("failed"), completedAt: z.string(), error: z.object({ code: z.literal("turn_failed"), message: z.literal("Pi turn failed") }) }),
|
|
23
|
+
z.object({ ...turnBase, state: z.literal("aborted"), completedAt: z.string() }),
|
|
24
|
+
]);
|
|
25
|
+
const promptSuccessSchema = z.object({ ok: z.literal(true), session: viewSchema, turn: turnSchema });
|
|
26
|
+
const turnSuccessSchema = z.object({ ok: z.literal(true), turn: turnSchema });
|
|
27
|
+
const listSchema = z.object({ ok: z.literal(true), sessions: z.array(viewSchema) });
|
|
28
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.object({ code: z.string(), message: z.string() }) });
|
|
29
|
+
const capabilitiesSchema = z.object({
|
|
30
|
+
ok: z.literal(true),
|
|
31
|
+
server: z.object({ name: z.literal(packageMetadata.name), version: z.literal(packageMetadata.version) }).strict(),
|
|
32
|
+
configuration: z.object({
|
|
33
|
+
fingerprint: z.string().regex(CONFIGURATION_FINGERPRINT_PATTERN),
|
|
34
|
+
reloadPolicy: z.literal(RELOAD_POLICY),
|
|
35
|
+
}).strict(),
|
|
36
|
+
workspaces: z.array(z.object({ alias: aliasSchema }).strict()),
|
|
37
|
+
executionProfiles: z.array(z.object({
|
|
38
|
+
alias: aliasSchema,
|
|
39
|
+
default: z.boolean(),
|
|
40
|
+
permissionProfile: z.enum(["read-only", "coding"]),
|
|
41
|
+
provider: z.string(),
|
|
42
|
+
model: z.string(),
|
|
43
|
+
thinkingLevel: thinkingLevelSchema,
|
|
44
|
+
}).strict()),
|
|
45
|
+
}).strict();
|
|
46
|
+
function result(value, isError = false) {
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
|
|
48
|
+
}
|
|
49
|
+
async function safe(action) {
|
|
50
|
+
try {
|
|
51
|
+
return result(await action());
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const known = error instanceof RegistryError || error instanceof PiSessionCreationError;
|
|
55
|
+
return result({ ok: false, error: { code: known ? error.code : "internal_error", message: known ? error.message : "Operation failed" } }, true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function createServer(registry, workspaces, executionProfiles, defaultExecutionProfile, configuration) {
|
|
59
|
+
const server = new McpServer({ name: packageMetadata.name, version: packageMetadata.version }, { capabilities: { tools: {} } });
|
|
60
|
+
server.registerTool("pi_capabilities_get", {
|
|
61
|
+
description: "List configured workspace and execution-profile aliases. This reports configured intent, not provider or runtime availability.",
|
|
62
|
+
inputSchema: z.object({}).strict(),
|
|
63
|
+
outputSchema: capabilitiesSchema,
|
|
64
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
65
|
+
}, () => result(projectCapabilities(workspaces, executionProfiles, configuration)));
|
|
66
|
+
server.registerTool("pi_session_start", {
|
|
67
|
+
description: "Start a managed embedded Pi session for a configured workspace alias.",
|
|
68
|
+
inputSchema: z.object({ workspace: aliasSchema, executionProfile: aliasSchema.optional() }).strict(), outputSchema: z.union([successSchema, errorSchema]),
|
|
69
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
70
|
+
}, ({ workspace, executionProfile }) => safe(async () => {
|
|
71
|
+
const alias = executionProfile ?? defaultExecutionProfile;
|
|
72
|
+
const configured = executionProfiles.get(alias);
|
|
73
|
+
if (!configured)
|
|
74
|
+
throw new RegistryError("unknown_execution_profile");
|
|
75
|
+
return { ok: true, session: await registry.start(workspace, toResolvedExecutionProfile(configured)) };
|
|
76
|
+
}));
|
|
77
|
+
server.registerTool("pi_session_list", {
|
|
78
|
+
description: "List local Pi sessions and their state.", inputSchema: z.object({}).strict(), outputSchema: z.union([listSchema, errorSchema]),
|
|
79
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
80
|
+
}, () => safe(() => ({ ok: true, sessions: registry.list() })));
|
|
81
|
+
server.registerTool("pi_session_get", {
|
|
82
|
+
description: "Get one local Pi session by ID.", inputSchema: z.object({ sessionId: z.string().uuid() }).strict(), outputSchema: z.union([successSchema, errorSchema]),
|
|
83
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
84
|
+
}, ({ sessionId }) => safe(() => ({ ok: true, session: registry.get(sessionId) })));
|
|
85
|
+
server.registerTool("pi_session_prompt", {
|
|
86
|
+
description: "Accept a prompt after Pi preflight and run the turn in the background.", inputSchema: z.object({ sessionId: z.string().uuid(), prompt: z.string().min(1).max(100_000) }).strict(), outputSchema: z.union([promptSuccessSchema, errorSchema]),
|
|
87
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
88
|
+
}, ({ sessionId, prompt }) => safe(async () => ({ ok: true, ...(await registry.prompt(sessionId, prompt)) })));
|
|
89
|
+
server.registerTool("pi_session_abort", {
|
|
90
|
+
description: "Abort a running Pi turn; idle sessions are unchanged.", inputSchema: z.object({ sessionId: z.string().uuid() }).strict(), outputSchema: z.union([successSchema, errorSchema]),
|
|
91
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
92
|
+
}, ({ sessionId }) => safe(async () => ({ ok: true, session: await registry.abort(sessionId) })));
|
|
93
|
+
server.registerTool("pi_session_close", {
|
|
94
|
+
description: "Abort if needed, dispose, and remove a Pi session.", inputSchema: z.object({ sessionId: z.string().uuid() }).strict(), outputSchema: z.union([successSchema, errorSchema]),
|
|
95
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
96
|
+
}, ({ sessionId }) => safe(async () => ({ ok: true, session: await registry.close(sessionId) })));
|
|
97
|
+
server.registerTool("pi_turn_get", {
|
|
98
|
+
description: "Get the normalized observable result of a Pi turn.", inputSchema: z.object({ sessionId: z.string().uuid(), turnId: z.string().uuid() }).strict(), outputSchema: z.union([turnSuccessSchema, errorSchema]),
|
|
99
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
100
|
+
}, ({ sessionId, turnId }) => safe(() => ({ ok: true, turn: registry.getTurn(sessionId, turnId) })));
|
|
101
|
+
return server;
|
|
102
|
+
}
|