@stigmer/server 3.38.0 → 3.38.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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/boot/compose.js +1 -1
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/boot/config.d.ts +18 -7
  5. package/dist/boot/config.d.ts.map +1 -1
  6. package/dist/boot/config.js +1 -0
  7. package/dist/boot/config.js.map +1 -1
  8. package/dist/domain/mcpserver/complete-oauth-connect.d.ts.map +1 -1
  9. package/dist/domain/mcpserver/complete-oauth-connect.js +10 -4
  10. package/dist/domain/mcpserver/complete-oauth-connect.js.map +1 -1
  11. package/dist/domain/mcpserver/connect.d.ts +2 -1
  12. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  13. package/dist/domain/mcpserver/connect.js +5 -2
  14. package/dist/domain/mcpserver/connect.js.map +1 -1
  15. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  16. package/dist/domain/mcpserver/controller.js +7 -3
  17. package/dist/domain/mcpserver/controller.js.map +1 -1
  18. package/dist/domain/mcpserver/get-oauth-grant-status.js +1 -1
  19. package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
  20. package/dist/domain/mcpserver/initiate-oauth-connect.js +8 -4
  21. package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
  22. package/dist/domain/mcpserver/start-connect.d.ts +2 -2
  23. package/dist/domain/mcpserver/start-connect.d.ts.map +1 -1
  24. package/dist/domain/mcpserver/start-connect.js +10 -4
  25. package/dist/domain/mcpserver/start-connect.js.map +1 -1
  26. package/dist/domain/platform/controller.d.ts +7 -5
  27. package/dist/domain/platform/controller.d.ts.map +1 -1
  28. package/dist/domain/platform/controller.js +4 -2
  29. package/dist/domain/platform/controller.js.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/pipeline/errors.d.ts +12 -13
  35. package/dist/pipeline/errors.d.ts.map +1 -1
  36. package/dist/pipeline/errors.js +29 -3
  37. package/dist/pipeline/errors.js.map +1 -1
  38. package/package.json +6 -6
  39. package/src/boot/__tests__/config.test.ts +31 -0
  40. package/src/boot/compose.ts +1 -1
  41. package/src/boot/config.ts +23 -7
  42. package/src/domain/agentexecution/__tests__/store-faults.test.ts +6 -26
  43. package/src/domain/mcpserver/__tests__/store-faults.test.ts +333 -0
  44. package/src/domain/mcpserver/complete-oauth-connect.ts +13 -4
  45. package/src/domain/mcpserver/connect.ts +7 -3
  46. package/src/domain/mcpserver/controller.ts +7 -3
  47. package/src/domain/mcpserver/get-oauth-grant-status.ts +1 -1
  48. package/src/domain/mcpserver/initiate-oauth-connect.ts +8 -4
  49. package/src/domain/mcpserver/start-connect.ts +12 -6
  50. package/src/domain/platform/__tests__/platform.test.ts +55 -3
  51. package/src/domain/platform/controller.ts +11 -7
  52. package/src/domain/workflowexecution/__tests__/store-faults.test.ts +6 -26
  53. package/src/index.ts +2 -0
  54. package/src/pipeline/__tests__/blind-not-found.test.ts +0 -5
  55. package/src/pipeline/__tests__/errors.test.ts +72 -0
  56. package/src/pipeline/__tests__/support.ts +48 -5
  57. package/src/pipeline/errors.ts +42 -3
@@ -56,13 +56,11 @@ export interface ServerConfig {
56
56
  readonly operatorName: string;
57
57
  /**
58
58
  * Temporal coordinates this server runs against (Go TemporalHostPort/
59
- * TemporalNamespace). Published to embedded runners via the platform
60
- * domain's getRunnerBootstrapConfig — in OSS the server and its runners
61
- * are co-located, so the address the server dials is the one runners
62
- * dial too. The Temporal workers (#18) read the same fields; connection
63
- * failure is NON-fatal (the server serves with the engine unavailable
64
- * and the TemporalManager's health monitor keeps retrying — Go
65
- * server.go InitialConnect posture).
59
+ * TemporalNamespace). The Temporal workers (#18) read the same fields;
60
+ * connection failure is NON-fatal (the server serves with the engine
61
+ * unavailable and the TemporalManager's health monitor keeps retrying —
62
+ * Go server.go InitialConnect posture). The address runners are told to
63
+ * dial is a separate fact: runnerBootstrapTemporalAddress below.
66
64
  */
67
65
  readonly temporalHostPort: string;
68
66
  readonly temporalNamespace: string;
@@ -75,6 +73,19 @@ export interface ServerConfig {
75
73
  * setting fails the boot, like OIDC's half configuration.
76
74
  */
77
75
  readonly temporalConnection: TemporalConnectionConfig;
76
+ /**
77
+ * The Temporal address getRunnerBootstrapConfig publishes to runners
78
+ * that discover their coordinates from this server instead of being
79
+ * given them (STIGMER_RUNNER_BOOTSTRAP_TEMPORAL_ADDRESS; stigmer#1357).
80
+ * Defaults to TEMPORAL_HOST_PORT — right whenever those runners share
81
+ * this server's network (a laptop, the compose stack, the chart). Set
82
+ * it when they do not: a server in a cluster that dials Temporal by its
83
+ * internal service name, and a desktop runner that must dial the
84
+ * frontend's external ingress. The namespace is not split; one
85
+ * namespace is served on every ingress. Sandboxes are told their
86
+ * address by the provisioner (sandboxTemporalAddress), never by this.
87
+ */
88
+ readonly runnerBootstrapTemporalAddress: string;
78
89
  /**
79
90
  * Artifact blob storage (attachments + execution outputs; Go
80
91
  * config.ArtifactStorage). "local" is the OSS default; "r2" boot-fails
@@ -315,6 +326,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
315
326
  temporalHostPort: envString(env, "TEMPORAL_HOST_PORT", "localhost:7233"),
316
327
  temporalNamespace: envString(env, "TEMPORAL_NAMESPACE", "default"),
317
328
  temporalConnection: loadTemporalConnectionConfig((name) => env[name]),
329
+ runnerBootstrapTemporalAddress: envString(
330
+ env,
331
+ "STIGMER_RUNNER_BOOTSTRAP_TEMPORAL_ADDRESS",
332
+ envString(env, "TEMPORAL_HOST_PORT", "localhost:7233"),
333
+ ),
318
334
  artifactStorageType,
319
335
  artifactLocalBasePath: envString(
320
336
  env,
@@ -36,7 +36,12 @@ import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agenti
36
36
  import type { ArtifactStorage } from "../../../artifactstorage/artifact-storage.js";
37
37
  import { createLogger } from "../../../boot/logger.js";
38
38
  import { callerIdentityKey } from "../../../pipeline/interceptors/auth.js";
39
- import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
39
+ import {
40
+ errorOf,
41
+ failingStore,
42
+ testCallerIdentity,
43
+ untouchable,
44
+ } from "../../../pipeline/__tests__/support.js";
40
45
  import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
41
46
  import { ResourceNotFoundError } from "../../../store/interface.js";
42
47
  import type { Store } from "../../../store/interface.js";
@@ -64,35 +69,10 @@ const EXECUTION_ID = "aex_storefault";
64
69
 
65
70
  const LOAD_FAULT_COPY = "failed to load agent execution";
66
71
 
67
- /** A dependency the call must never reach once its load has failed. */
68
- function untouchable<T extends object>(name: string): T {
69
- return new Proxy({} as T, {
70
- get(_target, prop) {
71
- throw new Error(`${name}.${String(prop)} reached after a failed load`);
72
- },
73
- });
74
- }
75
-
76
- /** A store whose every read fails with the given error. */
77
- function failingStore(error: Error): Store {
78
- return {
79
- getResource: () => Promise.reject(error),
80
- } as unknown as Store;
81
- }
82
-
83
72
  const MISSING = (): Error =>
84
73
  new ResourceNotFoundError(`agent_execution/${EXECUTION_ID}`);
85
74
  const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
86
75
 
87
- async function errorOf(call: () => Promise<unknown>): Promise<ConnectError> {
88
- const error = await call().then(
89
- () => undefined,
90
- (e: unknown) => e,
91
- );
92
- expect(error, "the call must fail").toBeInstanceOf(ConnectError);
93
- return error as ConnectError;
94
- }
95
-
96
76
  function lifecycleDeps(store: Store): LifecycleDeps {
97
77
  return {
98
78
  store,
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Pins the store-fault contract every MCP server load shares: a typed
3
+ * ResourceNotFoundError answers NotFound with the domain's pinned copy
4
+ * (`mcp_server not found: <id>`), and any other store failure is an
5
+ * infrastructure fault answered as a sanitized Internal, never a NotFound
6
+ * that tells a client the server does not exist (stigmer/stigmer#1345).
7
+ *
8
+ * The surfaces: connect, startConnect (its first load, and the attach arm's
9
+ * re-read after this lane lost the start race), initiateOAuthConnect,
10
+ * completeOAuthConnect, and updateVisibility's load step, reached through the
11
+ * registered handler on an in-process router. completeOAuthConnect loads
12
+ * after the pending state is consumed and the code exchanged, so its fault
13
+ * copy sends the user back through the connect flow. The composed suites
14
+ * reach only a real store, which cannot fail selectively, so each surface
15
+ * runs here against a store whose read throws. Every other dependency is
16
+ * untouchable: a load that fails must stop the call before any of them.
17
+ *
18
+ * Out of scope: the NotFound copy itself (wire contract, pinned by the
19
+ * mcpserver conformance suites and the domain's other tests) and the OAuth
20
+ * grant-store reads (stigmer/stigmer#1360).
21
+ */
22
+ import { create } from "@bufbuild/protobuf";
23
+ import type { ConnectError } from "@connectrpc/connect";
24
+ import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
25
+ import { describe, expect, it, vi } from "vitest";
26
+
27
+ import type { OutboundFetch } from "@stigmer/outbound/egress";
28
+ import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
29
+ import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
30
+ import {
31
+ CompleteOAuthConnectInputSchema,
32
+ ConnectInputSchema,
33
+ InitiateOAuthConnectInputSchema,
34
+ } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
35
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
36
+
37
+ import { createLogger } from "../../../boot/logger.js";
38
+ import { SecretService } from "../../../encryption/encryption.js";
39
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
40
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
41
+ import {
42
+ errorOf,
43
+ failingStore,
44
+ testCallerIdentity,
45
+ untouchable,
46
+ } from "../../../pipeline/__tests__/support.js";
47
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
48
+ import { ResourceNotFoundError } from "../../../store/interface.js";
49
+ import type {
50
+ PendingOAuthState,
51
+ PendingOAuthStateStore,
52
+ Store,
53
+ } from "../../../store/interface.js";
54
+
55
+ import { completeOAuthConnect } from "../complete-oauth-connect.js";
56
+ import type { McpServerConnectDeps } from "../connect.js";
57
+ import { connect } from "../connect.js";
58
+ import { registerMcpServerServices } from "../controller.js";
59
+ import type { ConnectRun, McpServerConnectEngine } from "../engine.js";
60
+ import { initiateOAuthConnect } from "../initiate-oauth-connect.js";
61
+ import { startConnect } from "../start-connect.js";
62
+
63
+ const silentLogger = createLogger({
64
+ level: "error",
65
+ pretty: false,
66
+ write: () => {},
67
+ });
68
+
69
+ const SERVER_ID = "mcps_storefault";
70
+ const ORG = "acme";
71
+
72
+ const NOT_FOUND_COPY = `mcp_server not found: ${SERVER_ID}`;
73
+ const LOAD_FAULT_COPY = "failed to load mcp server";
74
+ const CALLBACK_FAULT_COPY =
75
+ "failed to load mcp server — please retry the connect flow";
76
+
77
+ const MISSING = (): Error =>
78
+ new ResourceNotFoundError(`mcp_server/${SERVER_ID}`);
79
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
80
+
81
+ /**
82
+ * The connect slice's dependencies with every field untouchable except the
83
+ * ones a surface names: a load that fails must stop the call before the
84
+ * engine, the environments, the grant store or the network.
85
+ */
86
+ function connectDeps(
87
+ overrides: Partial<McpServerConnectDeps>,
88
+ ): McpServerConnectDeps {
89
+ return {
90
+ store: untouchable("store"),
91
+ logger: silentLogger,
92
+ authorizer: newPermissiveSingleTeamAuthorizer(),
93
+ engineState: untouchable("engineState"),
94
+ environmentReader: untouchable("environmentReader"),
95
+ executionContext: untouchable("executionContext"),
96
+ runnerAuth: untouchable("runnerAuth"),
97
+ managedEnv: untouchable("managedEnv"),
98
+ oauthGrants: untouchable("oauthGrants"),
99
+ pendingOAuthStates: untouchable("pendingOAuthStates"),
100
+ secretService: untouchable("secretService"),
101
+ oauthRedirectUri: "http://127.0.0.1:8234/auth/oauth/callback",
102
+ outboundFetch: untouchable("outboundFetch"),
103
+ ...overrides,
104
+ };
105
+ }
106
+
107
+ /** A plaintext pending state for SERVER_ID; a keyless SecretService unseals it to itself. */
108
+ const PENDING_STATE: PendingOAuthState = {
109
+ state: "state_storefault",
110
+ codeVerifier: "verifier_storefault",
111
+ clientId: "client_storefault",
112
+ clientSecret: "",
113
+ tokenEndpoint: "https://auth.example.test/token",
114
+ mcpServerId: SERVER_ID,
115
+ identityAccountId: "",
116
+ targetEnvVar: "MCP_ACCESS_TOKEN",
117
+ authMethod: "mcp_oauth",
118
+ tokenAuthMethod: "",
119
+ redirectUri: "http://127.0.0.1:8234/auth/oauth/callback",
120
+ org: ORG,
121
+ createdAt: 0,
122
+ };
123
+
124
+ const pendingStates: PendingOAuthStateStore = {
125
+ save: () => Promise.reject(new Error("pendingOAuthStates.save reached")),
126
+ getAndDelete: () => Promise.resolve(PENDING_STATE),
127
+ cleanupExpired: () =>
128
+ Promise.reject(new Error("pendingOAuthStates.cleanupExpired reached")),
129
+ };
130
+
131
+ /** The provider's token endpoint: the exchange succeeds, so the load is next. */
132
+ const tokenEndpoint: OutboundFetch = () =>
133
+ Promise.resolve(
134
+ new Response(
135
+ JSON.stringify({ access_token: "at_storefault", token_type: "bearer" }),
136
+ { status: 200 },
137
+ ),
138
+ );
139
+
140
+ const connectInput = () =>
141
+ create(ConnectInputSchema, { mcpServerId: SERVER_ID, org: ORG });
142
+
143
+ /**
144
+ * The direct handlers' entry loads. Each validates its input first, so the
145
+ * inputs carry every field the handler requires; completeOAuthConnect also
146
+ * consumes the pending state, unseals it and exchanges the code before its
147
+ * load, so its fakes answer those three steps.
148
+ */
149
+ const HANDLER_LOADS: ReadonlyArray<
150
+ readonly [string, string, (store: Store) => Promise<unknown>]
151
+ > = [
152
+ [
153
+ "connect",
154
+ LOAD_FAULT_COPY,
155
+ (store) =>
156
+ connect(connectDeps({ store }), connectInput(), testCallerIdentity()),
157
+ ],
158
+ [
159
+ "startConnect — the first load",
160
+ LOAD_FAULT_COPY,
161
+ (store) =>
162
+ startConnect(
163
+ connectDeps({ store }),
164
+ connectInput(),
165
+ testCallerIdentity(),
166
+ ),
167
+ ],
168
+ [
169
+ "initiateOAuthConnect",
170
+ LOAD_FAULT_COPY,
171
+ (store) =>
172
+ initiateOAuthConnect(
173
+ connectDeps({ store }),
174
+ create(InitiateOAuthConnectInputSchema, {
175
+ mcpServerId: SERVER_ID,
176
+ org: ORG,
177
+ }),
178
+ testCallerIdentity(),
179
+ ),
180
+ ],
181
+ [
182
+ "completeOAuthConnect — after the code exchange",
183
+ CALLBACK_FAULT_COPY,
184
+ (store) =>
185
+ completeOAuthConnect(
186
+ connectDeps({
187
+ store,
188
+ pendingOAuthStates: pendingStates,
189
+ secretService: SecretService.create(undefined),
190
+ outboundFetch: tokenEndpoint,
191
+ }),
192
+ create(CompleteOAuthConnectInputSchema, {
193
+ mcpServerId: SERVER_ID,
194
+ state: PENDING_STATE.state,
195
+ authorizationCode: "code_storefault",
196
+ }),
197
+ testCallerIdentity(),
198
+ ),
199
+ ],
200
+ ];
201
+
202
+ describe.each(HANDLER_LOADS)("%s", (_surface, faultCopy, run) => {
203
+ it("a missing server answers NotFound with the domain's copy", async () => {
204
+ const error = await errorOf(() => run(failingStore(MISSING())));
205
+
206
+ expect(error.code).toBe(Code.NotFound);
207
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
208
+ });
209
+
210
+ it("any other store failure answers a sanitized Internal", async () => {
211
+ const error = await errorOf(() => run(failingStore(LOCKED())));
212
+
213
+ expect(error.code).toBe(Code.Internal);
214
+ expect(error.rawMessage).toBe(faultCopy);
215
+ });
216
+ });
217
+
218
+ describe("startConnect — the attach arm's re-read after losing the start race", () => {
219
+ /** No env and no auth block: prepareConnect creates no ExecutionContext and reads no grant. */
220
+ const SERVER = create(McpServerSchema, {
221
+ metadata: { id: SERVER_ID, name: "Store Fault", org: ORG },
222
+ spec: {
223
+ serverType: { case: "stdio", value: { command: "npx", args: [] } },
224
+ },
225
+ });
226
+
227
+ async function attachError(reReadError: Error): Promise<ConnectError> {
228
+ const attached: ConnectRun = {
229
+ workflowId: `stigmer/mcp-server/connect/${SERVER_ID}`,
230
+ attached: true,
231
+ result: () =>
232
+ Promise.reject(new Error("result reached on the attach arm")),
233
+ };
234
+ const startOrAttachConnect = vi.fn<
235
+ McpServerConnectEngine["startOrAttachConnect"]
236
+ >(() => Promise.resolve(attached));
237
+ const engine: McpServerConnectEngine = {
238
+ startOrAttachConnect,
239
+ isConnectRunRunning: () =>
240
+ Promise.reject(
241
+ new Error("isConnectRunRunning reached without a CONNECTING record"),
242
+ ),
243
+ hasRunnerQueuePollers: () => Promise.resolve(true),
244
+ };
245
+ const getResource = vi
246
+ .fn()
247
+ .mockResolvedValueOnce(SERVER)
248
+ .mockRejectedValueOnce(reReadError);
249
+
250
+ const error = await errorOf(() =>
251
+ startConnect(
252
+ connectDeps({
253
+ store: { getResource } as unknown as Store,
254
+ engineState: () => ({ connected: true, engine }),
255
+ }),
256
+ connectInput(),
257
+ testCallerIdentity(),
258
+ ),
259
+ );
260
+
261
+ // The start ran once and the store was read twice: the failure is the
262
+ // re-read's, not the first load's.
263
+ expect(startOrAttachConnect).toHaveBeenCalledTimes(1);
264
+ expect(getResource).toHaveBeenCalledTimes(2);
265
+ return error;
266
+ }
267
+
268
+ it("a server deleted since the first load answers NotFound with the domain's copy", async () => {
269
+ const error = await attachError(MISSING());
270
+
271
+ expect(error.code).toBe(Code.NotFound);
272
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
273
+ });
274
+
275
+ it("any other store failure answers a sanitized Internal", async () => {
276
+ const error = await attachError(LOCKED());
277
+
278
+ expect(error.code).toBe(Code.Internal);
279
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
280
+ });
281
+ });
282
+
283
+ describe("updateVisibility — LoadMcpServerForVisibilityUpdate", () => {
284
+ /**
285
+ * The registered handler on an in-process router (the artifact domain's
286
+ * harness): the verifier chain stamps the trusted-local caller and the
287
+ * apiresource interceptor the kind, then Authorize and ValidateProto run
288
+ * and the load is the first step that reads the store. The connect slice
289
+ * is never reached.
290
+ */
291
+ function updateVisibilityError(store: Store): Promise<ConnectError> {
292
+ const transport = createRouterTransport(
293
+ (router) => {
294
+ registerMcpServerServices(router, {
295
+ store,
296
+ logger: silentLogger,
297
+ authorizer: newPermissiveSingleTeamAuthorizer(),
298
+ authorizationLifecycle: undefined,
299
+ connect: untouchable("connect"),
300
+ });
301
+ },
302
+ {
303
+ router: {
304
+ interceptors: [
305
+ createVerifierChainInterceptor([], [], silentLogger),
306
+ createApiResourceInterceptor(),
307
+ ],
308
+ },
309
+ },
310
+ );
311
+ const command = createClient(McpServerCommandController, transport);
312
+ return errorOf(() =>
313
+ command.updateVisibility({
314
+ resourceId: SERVER_ID,
315
+ visibility: ApiResourceVisibility.visibility_private,
316
+ }),
317
+ );
318
+ }
319
+
320
+ it("a missing server answers NotFound with the domain's copy", async () => {
321
+ const error = await updateVisibilityError(failingStore(MISSING()));
322
+
323
+ expect(error.code).toBe(Code.NotFound);
324
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
325
+ });
326
+
327
+ it("any other store failure answers a sanitized Internal", async () => {
328
+ const error = await updateVisibilityError(failingStore(LOCKED()));
329
+
330
+ expect(error.code).toBe(Code.Internal);
331
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
332
+ });
333
+ });
@@ -14,8 +14,8 @@
14
14
  * pin). Disclosed divergence; the Go-side issue files at wrap-up.
15
15
  *
16
16
  * Proven by mcpserver-connect.conformance.test.ts
17
- * (CONFORMANCE_TARGET=local-execution) and
18
- * __tests__/complete-oauth-connect.test.ts.
17
+ * (CONFORMANCE_TARGET=local-execution), __tests__/oauth-handshake.test.ts
18
+ * and __tests__/store-faults.test.ts.
19
19
  */
20
20
  import { create } from "@bufbuild/protobuf";
21
21
 
@@ -42,6 +42,7 @@ import {
42
42
  } from "../../pipeline/errors.js";
43
43
  import { authorizeDirect } from "../../pipeline/steps/authorize.js";
44
44
  import type { PendingOAuthState } from "../../store/interface.js";
45
+ import { ResourceNotFoundError } from "../../store/interface.js";
45
46
  import type { McpServerConnectDeps } from "./connect.js";
46
47
  import { exchangeCode } from "./oauth/token.js";
47
48
 
@@ -147,8 +148,16 @@ export async function completeOAuthConnect(
147
148
  mcpServerId,
148
149
  McpServerSchema,
149
150
  );
150
- } catch {
151
- throw notFoundError("mcp_server", mcpServerId);
151
+ } catch (error) {
152
+ if (error instanceof ResourceNotFoundError) {
153
+ throw notFoundError("mcp_server", mcpServerId);
154
+ }
155
+ // The pending state is consumed and the code exchanged by now, so a
156
+ // retry of this call can only fail: the copy names the way back.
157
+ throw internalError(
158
+ error,
159
+ "failed to load mcp server — please retry the connect flow",
160
+ );
152
161
  }
153
162
 
154
163
  let org = pendingState.org;
@@ -12,7 +12,8 @@
12
12
  * (engine.ts) and never registers a worker.
13
13
  *
14
14
  * Proven by mcpserver-connect.conformance.test.ts
15
- * (CONFORMANCE_TARGET=local-execution) and __tests__/connect.test.ts.
15
+ * (CONFORMANCE_TARGET=local-execution), __tests__/connect.test.ts and
16
+ * __tests__/store-faults.test.ts.
16
17
  */
17
18
  import type { OutboundFetch } from "@stigmer/outbound/egress";
18
19
  import { create } from "@bufbuild/protobuf";
@@ -269,8 +270,11 @@ export async function connect(
269
270
  mcpServerId,
270
271
  McpServerSchema,
271
272
  );
272
- } catch {
273
- throw notFoundError("mcp_server", mcpServerId);
273
+ } catch (error) {
274
+ if (error instanceof ResourceNotFoundError) {
275
+ throw notFoundError("mcp_server", mcpServerId);
276
+ }
277
+ throw internalError(error, "failed to load mcp server");
274
278
  }
275
279
 
276
280
  // The annotation's can_connect check after the load and BEFORE the engine
@@ -106,6 +106,7 @@ import {
106
106
  newValidateVisibilityUpdateStep,
107
107
  } from "../../pipeline/steps/validate-visibility.js";
108
108
  import type { Store } from "../../store/interface.js";
109
+ import { ResourceNotFoundError } from "../../store/interface.js";
109
110
  import { completeOAuthConnect } from "./complete-oauth-connect.js";
110
111
  import { connect, startBestEffortConnect } from "./connect.js";
111
112
  import type { McpServerConnectDeps } from "./connect.js";
@@ -479,7 +480,7 @@ async function updateVisibility(
479
480
  return reqCtx.get(UPDATE_VISIBILITY_MCP_SERVER_KEY) as McpServer;
480
481
  }
481
482
 
482
- /** Loads the server by resource_id; ANY load failure → NotFound. */
483
+ /** Loads the server by resource_id; a missing one answers NotFound, a store fault Internal. */
483
484
  function newLoadMcpServerForVisibilityUpdateStep(
484
485
  store: Store,
485
486
  ): PipelineStep<UpdateVisibilityDesc> {
@@ -494,8 +495,11 @@ function newLoadMcpServerForVisibilityUpdateStep(
494
495
  input.resourceId,
495
496
  McpServerSchema,
496
497
  );
497
- } catch {
498
- throw notFoundError("mcp_server", input.resourceId);
498
+ } catch (error) {
499
+ if (error instanceof ResourceNotFoundError) {
500
+ throw notFoundError("mcp_server", input.resourceId);
501
+ }
502
+ throw internalError(error, "failed to load mcp server");
499
503
  }
500
504
  ctx.set(UPDATE_VISIBILITY_MCP_SERVER_KEY, mcpServer);
501
505
  },
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Proven by mcpserver-oauth.conformance.test.ts
12
12
  * (CONFORMANCE_TARGET=local) and
13
- * __tests__/get-oauth-grant-status.test.ts.
13
+ * __tests__/oauth-handshake.test.ts.
14
14
  */
15
15
  import { create } from "@bufbuild/protobuf";
16
16
 
@@ -8,8 +8,8 @@
8
8
  * credentials and build the auth URL from its endpoints.
9
9
  *
10
10
  * Proven by mcpserver-oauth.conformance.test.ts
11
- * (CONFORMANCE_TARGET=local) and
12
- * __tests__/initiate-oauth-connect.test.ts.
11
+ * (CONFORMANCE_TARGET=local), __tests__/oauth-handshake.test.ts and
12
+ * __tests__/store-faults.test.ts.
13
13
  */
14
14
  import { create } from "@bufbuild/protobuf";
15
15
  import { randomBytes } from "node:crypto";
@@ -40,6 +40,7 @@ import {
40
40
  } from "../../pipeline/errors.js";
41
41
  import { authorizeDirect } from "../../pipeline/steps/authorize.js";
42
42
  import type { PendingOAuthState } from "../../store/interface.js";
43
+ import { ResourceNotFoundError } from "../../store/interface.js";
43
44
  import { resolveOAuthAppRef } from "../oauthapp/refresolution.js";
44
45
  import { tokenAuthMethodFromSpec } from "./connect.js";
45
46
  import type { McpServerConnectDeps } from "./connect.js";
@@ -73,8 +74,11 @@ export async function initiateOAuthConnect(
73
74
  mcpServerId,
74
75
  McpServerSchema,
75
76
  );
76
- } catch {
77
- throw notFoundError("mcp_server", mcpServerId);
77
+ } catch (error) {
78
+ if (error instanceof ResourceNotFoundError) {
79
+ throw notFoundError("mcp_server", mcpServerId);
80
+ }
81
+ throw internalError(error, "failed to load mcp server");
78
82
  }
79
83
 
80
84
  // The annotation's can_connect check AFTER the load — the Java
@@ -21,8 +21,8 @@
21
21
  * which is both the repair and the caller's intent.
22
22
  *
23
23
  * Proven by mcpserver-connect.conformance.test.ts
24
- * (CONFORMANCE_TARGET=local-execution) and
25
- * __tests__/start-connect.test.ts.
24
+ * (CONFORMANCE_TARGET=local-execution), __tests__/connect.test.ts and
25
+ * __tests__/store-faults.test.ts.
26
26
  */
27
27
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
28
28
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
@@ -84,8 +84,11 @@ export async function startConnect(
84
84
  mcpServerId,
85
85
  McpServerSchema,
86
86
  );
87
- } catch {
88
- throw notFoundError("mcp_server", mcpServerId);
87
+ } catch (error) {
88
+ if (error instanceof ResourceNotFoundError) {
89
+ throw notFoundError("mcp_server", mcpServerId);
90
+ }
91
+ throw internalError(error, "failed to load mcp server");
89
92
  }
90
93
 
91
94
  // The annotation's can_connect check after the load and BEFORE the engine
@@ -165,8 +168,11 @@ export async function startConnect(
165
168
  mcpServerId,
166
169
  McpServerSchema,
167
170
  );
168
- } catch {
169
- throw notFoundError("mcp_server", mcpServerId);
171
+ } catch (error) {
172
+ if (error instanceof ResourceNotFoundError) {
173
+ throw notFoundError("mcp_server", mcpServerId);
174
+ }
175
+ throw internalError(error, "failed to load mcp server");
170
176
  }
171
177
  }
172
178