@adcp/sdk 8.1.0-beta.18 → 8.1.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/core/TaskExecutor.js +5 -5
- package/dist/lib/core/TaskExecutor.js.map +1 -1
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/a2a-adapter.d.ts.map +1 -1
- package/dist/lib/server/a2a-adapter.js +44 -7
- package/dist/lib/server/a2a-adapter.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +3 -13
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/idempotency/backends/lazy.d.ts +33 -0
- package/dist/lib/server/idempotency/backends/lazy.d.ts.map +1 -0
- package/dist/lib/server/idempotency/backends/lazy.js +82 -0
- package/dist/lib/server/idempotency/backends/lazy.js.map +1 -0
- package/dist/lib/server/idempotency/backends/redis.d.ts +6 -1
- package/dist/lib/server/idempotency/backends/redis.d.ts.map +1 -1
- package/dist/lib/server/idempotency/backends/redis.js +6 -1
- package/dist/lib/server/idempotency/backends/redis.js.map +1 -1
- package/dist/lib/server/idempotency/index.d.ts +2 -0
- package/dist/lib/server/idempotency/index.d.ts.map +1 -1
- package/dist/lib/server/idempotency/index.js +3 -1
- package/dist/lib/server/idempotency/index.js.map +1 -1
- package/dist/lib/server/index.d.ts +2 -2
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +3 -2
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +27 -5
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/docs/guides/BUILD-AN-AGENT.md +25 -0
- package/docs/llms.txt +5 -3
- package/examples/README.md +4 -0
- package/examples/decisioning-platform-path-routed.ts +278 -0
- package/package.json +1 -1
package/docs/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Ad Context Protocol (AdCP)
|
|
2
2
|
|
|
3
|
-
> Generated at: 2026-05-
|
|
4
|
-
> Library: @adcp/sdk v8.1.0-beta.
|
|
3
|
+
> Generated at: 2026-05-31
|
|
4
|
+
> Library: @adcp/sdk v8.1.0-beta.18
|
|
5
5
|
> AdCP major version: 3
|
|
6
6
|
> Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
|
|
7
7
|
> Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
|
|
@@ -56,6 +56,8 @@ Lower-level option: `createAdcpServer({ signals: { getSignals: ... } })` from `@
|
|
|
56
56
|
|
|
57
57
|
**Four reference `AccountStore` shapes.** Pick the one whose onboarding model matches yours. **Shape A — `InMemoryImplicitAccountStore`**: `resolution: 'implicit'`, buyer-driven `sync_accounts` populates the auth-principal → accounts map. **Shape B — `createOAuthPassthroughResolver`**: `resolution: 'explicit'`, returns just the `resolve` function for adapters fronting an upstream OAuth listing endpoint (Snap, Meta, TikTok, LinkedIn — `extract bearer → GET /me/adaccounts → match by id`). **Shape C — `createRosterAccountStore`**: `resolution: 'explicit'`, returns a complete `AccountStore` for adopters who own the roster (storefront table, admin-UI-managed JSON). Supports `resolveWithoutRef` for tools that send no `account` field on the wire (`list_creative_formats`, `preview_creative`, `provide_performance_feedback`) — set it to return a synthetic publisher-wide entry instead of `null`. **Shape D — `createDerivedAccountStore`**: `resolution: 'derived'`, single-tenant agents where there is no `account_id` on the wire and the auth principal alone identifies the tenant (audiostack, flashtalking, single-namespace retail-media). Provide `toAccount(ctx)`; the factory still emits legacy-compatible `AUTH_REQUIRED` on missing-credential calls and ignores buyer-supplied `account_id` (single-tenant by definition). Buyer code must continue to handle `AUTH_REQUIRED` alongside `AUTH_MISSING` / `AUTH_INVALID`. All four live at `@adcp/sdk/server`.
|
|
58
58
|
|
|
59
|
+
**Stateless BYOK provider auth.** For single-account API-key or bearer-token BYOK, the provider credential can be the AdCP request credential for that endpoint: `Authorization: Bearer <provider_api_key_or_access_token>`. This keeps the baseline seller-agent wrapper pattern single-plane: the seller agent authenticates the request with the caller-presented provider credential, derives the account from request auth, and uses the same request-local token for upstream provider calls. No SDK-managed OAuth flow, refresh-token store, provider-token store, or callback route is required when the caller owns the provider credential lifecycle. If the provider credential can see multiple upstream accounts, use an explicit account roster pattern such as `createOAuthPassthroughResolver` instead of `'derived'`. Handlers with a resolved account should read the active token from `ctx.account.authInfo?.token`; refresh hooks update `account.authInfo`. Handlers without a resolved account can read the request token from `ctx.authInfo.token`. Use a stable non-secret identity such as `ctx.authInfo.credential.key_id`, `ctx.authInfo.credential.client_id`, or an adopter-supplied `principal` string for cache/idempotency scoping. Treat both token paths as request-local: do not copy provider tokens into persisted Account rows, `ctx_metadata`, `ctx.authInfo.extra`, request `ext` / body fields, or log lines. Add a separate provider-auth channel only for dual-auth proxy deployments where one request carries both caller-to-agent auth and a distinct upstream-provider credential.
|
|
60
|
+
|
|
59
61
|
**Multi-tenant.** Two helpers, pick by deployment shape. **Host-routed**: `createTenantRegistry({...})` — one server per tenant, tenant-id keyed lookup with `registry.get(tenantId)`. **Account-routed**: `createTenantStore({...})` — one server, per-entry tenant-isolation gate built in (cross-tenant entries on `upsert` / `syncGovernance` rejected with `PERMISSION_DENIED` BEFORE adopter callbacks run; fail-closed when the auth principal can't be resolved). `createTenantStore` mitigates the canonical multi-tenant write-across-tenants bug class at the SDK layer rather than relying on adopter discipline.
|
|
60
62
|
|
|
61
63
|
**`BuyerAgentRegistry`** — durable buyer-agent identity surface. `BuyerAgentRegistry.signingOnly({ resolveByAgentUrl })` (production target — only `http_sig` credentials route through), `bearerOnly({ resolveByCredential })` (pre-trust beta — bearer/api-key/oauth all route), `mixed(...)` (transition posture). Wrap with `BuyerAgentRegistry.cached(inner, { ttlSeconds })` for TTL + LRU + concurrent-resolve coalescing. The resolved `BuyerAgent` flows through `ctx.agent` to every `AccountStore` method (`resolve` / `upsert` / `list` / `syncGovernance` / `reportUsage` / `getAccountFinancials`) and to `tasks_get` polling. `BuyerAgent.status === 'suspended' | 'blocked'` triggers framework-level `PERMISSION_DENIED`. `BuyerAgent.sandbox_only: true` rejects requests against non-sandbox accounts. See [`docs/migration-buyer-agent-registry.md`](./migration-buyer-agent-registry.md) for the full surface.
|
|
@@ -1492,7 +1494,7 @@ Every tool call returns a `TaskResult` with one of these statuses:
|
|
|
1492
1494
|
|
|
1493
1495
|
## Protocols
|
|
1494
1496
|
|
|
1495
|
-
AdCP tools are served over MCP (Model Context Protocol) or A2A (Agent-to-Agent). The client auto-detects based on `AgentConfig.protocol`. MCP endpoints end with `/mcp/`.
|
|
1497
|
+
AdCP tools are served over MCP (Model Context Protocol) or A2A (Agent-to-Agent). The client auto-detects based on `AgentConfig.protocol`. MCP endpoints end with `/mcp/`. Bearer auth uses `Authorization: Bearer <token>`; SDK clients also send the legacy `x-adcp-auth` header for compatibility, and servers accept it as a fallback.
|
|
1496
1498
|
|
|
1497
1499
|
**Deep dive:** docs/development/PROTOCOL_DIFFERENCES.md
|
|
1498
1500
|
|
package/examples/README.md
CHANGED
|
@@ -70,6 +70,10 @@ Cross-specialism dispatch: `brandRights.acquireRights` consults `campaignGoverna
|
|
|
70
70
|
|
|
71
71
|
This is distinct from `decisioning-platform-multi-tenant.ts` which uses **host-routed** tenancy via `TenantRegistry` (different agentUrls per tenant). Both are valid; pick by deployment shape.
|
|
72
72
|
|
|
73
|
+
### Multi-tenant (path-routed Express)
|
|
74
|
+
|
|
75
|
+
`decisioning-platform-path-routed.ts` demonstrates the **path-routed** TenantRegistry model for existing Express apps that must keep legacy public paths such as `/storefront/:platformId/mcp`. The route resolves the tenant from the decoded path segment, creates a per-request `StreamableHTTPServerTransport`, stamps MCP `req.auth` from the app's existing auth middleware, and lets each platform's `accounts.resolve(ref, ctx)` scope buyer identity from `ctx.authInfo`.
|
|
76
|
+
|
|
73
77
|
### Agent testing (`comply_test_controller`)
|
|
74
78
|
|
|
75
79
|
Start with `createComplyController` (`comply-controller-seller.ts`). Switch to `registerTestController` (`seller-test-controller.ts`) only when your domain state has internal structure that multiple production tools read from — i.e., when the adapter surface's one-method-per-scenario shape starts fighting the code you already have.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-routed TenantRegistry + Express StreamableHTTP auth example.
|
|
3
|
+
*
|
|
4
|
+
* Use this shape when an existing buyer-facing Express app already owns a
|
|
5
|
+
* public path such as `/storefront/:platformId/mcp` and existing auth
|
|
6
|
+
* middleware, but you want SDK-owned tenant servers instead of constructing
|
|
7
|
+
* one AdCP server per buyer request.
|
|
8
|
+
*
|
|
9
|
+
* The important boundary is:
|
|
10
|
+
*
|
|
11
|
+
* Express auth middleware -> req.user -> req.auth -> ctx.authInfo -> accounts.resolve()
|
|
12
|
+
*
|
|
13
|
+
* Tenant selection stays at the route/registry boundary. Buyer identity stays
|
|
14
|
+
* in MCP `AuthInfo`, so platform handlers do not close over Express `req.user`.
|
|
15
|
+
*
|
|
16
|
+
* Mount this route after your JSON body parser, for example
|
|
17
|
+
* `app.use(express.json({ limit: '2mb' }))`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Express, NextFunction, Request, RequestHandler, Response } from 'express';
|
|
21
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
22
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
23
|
+
import { mcpAcceptHeaderMiddleware } from '@adcp/sdk/express-mcp';
|
|
24
|
+
import {
|
|
25
|
+
createTenantRegistry,
|
|
26
|
+
type AccountStore,
|
|
27
|
+
type DecisioningAdcpServer,
|
|
28
|
+
type ResolveContext,
|
|
29
|
+
type TenantRegistry,
|
|
30
|
+
type TenantSigningKey,
|
|
31
|
+
} from '@adcp/sdk/server';
|
|
32
|
+
import type { AccountReference } from '@adcp/sdk/types';
|
|
33
|
+
import { ProgrammaticSeller } from './decisioning-platform-programmatic';
|
|
34
|
+
import { BroadcastTvSeller } from './decisioning-platform-broadcast-tv';
|
|
35
|
+
|
|
36
|
+
type PlatformId = 'snap' | 'vertex';
|
|
37
|
+
|
|
38
|
+
interface StorefrontUser {
|
|
39
|
+
buyerCustomerId: string;
|
|
40
|
+
accessToken: string;
|
|
41
|
+
scopes: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type StorefrontRequest = Request<{ platformId: string }, unknown, unknown> & {
|
|
45
|
+
user?: StorefrontUser;
|
|
46
|
+
auth?: AuthInfo;
|
|
47
|
+
body?: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface ProgrammaticCtxMeta {
|
|
51
|
+
network_id: string;
|
|
52
|
+
advertiser_id: string;
|
|
53
|
+
buyer_customer_id: string;
|
|
54
|
+
tenant_id: PlatformId;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface BroadcastCtxMeta {
|
|
59
|
+
agency_buyer_id: string;
|
|
60
|
+
affiliate_advertiser_id: string;
|
|
61
|
+
buyer_customer_id: string;
|
|
62
|
+
tenant_id: PlatformId;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Existing apps usually already have this function behind their auth
|
|
68
|
+
* middleware. The example accepts it as a dependency instead of inventing
|
|
69
|
+
* bearer tokens or fake users.
|
|
70
|
+
*/
|
|
71
|
+
export type VerifyStorefrontUser = (req: Request) => Promise<StorefrontUser | null>;
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// PRODUCTION: REPLACE WITH KMS-BACKED LOADER
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
//
|
|
77
|
+
// The registry can serve unsigned tenants in AdCP 3.x, so this example returns
|
|
78
|
+
// undefined to keep the route runnable before KMS/brand.json is wired. In a
|
|
79
|
+
// production signed-webhook deployment, load one tenant-specific key from your
|
|
80
|
+
// KMS or secret manager and publish the public half in that tenant's brand.json.
|
|
81
|
+
// Never commit private signing material to source.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
async function loadTenantSigningKey(_tenantId: PlatformId): Promise<TenantSigningKey | undefined> {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optionalSigningKey(signingKey: TenantSigningKey | undefined): { signingKey?: TenantSigningKey } {
|
|
88
|
+
return signingKey ? { signingKey } : {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class StorefrontProgrammaticSeller extends ProgrammaticSeller {
|
|
92
|
+
override accounts: AccountStore<ProgrammaticCtxMeta> = {
|
|
93
|
+
resolve: async (ref: AccountReference | undefined, ctx?: ResolveContext) => {
|
|
94
|
+
const buyerCustomerId = buyerCustomerIdFrom(ctx);
|
|
95
|
+
if (!buyerCustomerId) return null;
|
|
96
|
+
const accountId = accountIdFrom(ref) ?? `snap_${buyerCustomerId}`;
|
|
97
|
+
return {
|
|
98
|
+
id: accountId,
|
|
99
|
+
name: `Snap storefront account ${accountId}`,
|
|
100
|
+
status: 'active',
|
|
101
|
+
operator: 'snap.example.com',
|
|
102
|
+
ctx_metadata: {
|
|
103
|
+
network_id: this.capabilities.config.networkId,
|
|
104
|
+
advertiser_id: buyerCustomerId,
|
|
105
|
+
buyer_customer_id: buyerCustomerId,
|
|
106
|
+
tenant_id: 'snap',
|
|
107
|
+
},
|
|
108
|
+
authInfo: { kind: 'oauth', token: ctx?.authInfo?.token, principal: buyerCustomerId },
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class StorefrontBroadcastSeller extends BroadcastTvSeller {
|
|
115
|
+
override accounts: AccountStore<BroadcastCtxMeta> = {
|
|
116
|
+
resolve: async (ref: AccountReference | undefined, ctx?: ResolveContext) => {
|
|
117
|
+
const buyerCustomerId = buyerCustomerIdFrom(ctx);
|
|
118
|
+
if (!buyerCustomerId) return null;
|
|
119
|
+
const accountId = accountIdFrom(ref) ?? `vertex_${buyerCustomerId}`;
|
|
120
|
+
return {
|
|
121
|
+
id: accountId,
|
|
122
|
+
name: `Vertex storefront account ${accountId}`,
|
|
123
|
+
status: 'active',
|
|
124
|
+
operator: 'vertex.example.com',
|
|
125
|
+
ctx_metadata: {
|
|
126
|
+
agency_buyer_id: buyerCustomerId,
|
|
127
|
+
affiliate_advertiser_id: accountId,
|
|
128
|
+
buyer_customer_id: buyerCustomerId,
|
|
129
|
+
tenant_id: 'vertex',
|
|
130
|
+
},
|
|
131
|
+
authInfo: { kind: 'oauth', token: ctx?.authInfo?.token, principal: buyerCustomerId },
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function buildPathRoutedStorefrontRegistry(): Promise<TenantRegistry> {
|
|
138
|
+
const registry = createTenantRegistry({
|
|
139
|
+
defaultServerOptions: {
|
|
140
|
+
name: 'path-routed-storefront',
|
|
141
|
+
version: '0.0.1',
|
|
142
|
+
validation: { requests: 'strict', responses: 'strict' },
|
|
143
|
+
},
|
|
144
|
+
autoValidate: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
registry.register('snap', {
|
|
148
|
+
agentUrl: 'https://storefront.example.com/storefront/snap/mcp',
|
|
149
|
+
...optionalSigningKey(await loadTenantSigningKey('snap')),
|
|
150
|
+
platform: new StorefrontProgrammaticSeller(),
|
|
151
|
+
label: 'Snap Storefront',
|
|
152
|
+
serverOptions: { name: 'snap-storefront', version: '1.0.0' },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
registry.register('vertex', {
|
|
156
|
+
agentUrl: 'https://storefront.example.com/storefront/vertex/mcp',
|
|
157
|
+
...optionalSigningKey(await loadTenantSigningKey('vertex')),
|
|
158
|
+
platform: new StorefrontBroadcastSeller(),
|
|
159
|
+
label: 'Vertex Storefront',
|
|
160
|
+
serverOptions: { name: 'vertex-storefront', version: '1.0.0' },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return registry;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface MountPathRoutedStorefrontOptions {
|
|
167
|
+
/**
|
|
168
|
+
* Existing app-specific verifier. Return null for unauthenticated requests;
|
|
169
|
+
* the middleware below maps that to 401 before the MCP transport runs.
|
|
170
|
+
*/
|
|
171
|
+
verifyUser: VerifyStorefrontUser;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function mountPathRoutedStorefront(
|
|
175
|
+
app: Pick<Express, 'all'>,
|
|
176
|
+
registry: TenantRegistry,
|
|
177
|
+
options: MountPathRoutedStorefrontOptions
|
|
178
|
+
): void {
|
|
179
|
+
const acceptMiddleware = mcpAcceptHeaderMiddleware() as RequestHandler;
|
|
180
|
+
const authMiddleware = createStorefrontAuthMiddleware(options.verifyUser);
|
|
181
|
+
|
|
182
|
+
app.all('/storefront/:platformId/mcp', acceptMiddleware, authMiddleware, async (req: Request, res: Response) => {
|
|
183
|
+
const storefrontReq = req as StorefrontRequest;
|
|
184
|
+
const platformId = normalizePlatformId(storefrontReq.params.platformId);
|
|
185
|
+
if (!platformId) {
|
|
186
|
+
res.status(404).json({ error: 'unknown platform' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Express already decoded `:platformId`, so prefer direct tenant lookup.
|
|
191
|
+
// If your app uses a catch-all route instead, use
|
|
192
|
+
// `registry.resolveByRequest(req.headers.host!, req.path)` here.
|
|
193
|
+
const resolved = registry.get(platformId);
|
|
194
|
+
if (!resolved) {
|
|
195
|
+
res.status(404).json({ error: 'tenant unavailable' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
storefrontReq.auth = toMcpAuthInfo(storefrontReq.user!);
|
|
200
|
+
await handleWithPerRequestTransport(resolved.server, storefrontReq, res, storefrontReq.body);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createStorefrontAuthMiddleware(verifyUser: VerifyStorefrontUser): RequestHandler {
|
|
205
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
206
|
+
try {
|
|
207
|
+
const user = await verifyUser(req);
|
|
208
|
+
if (!user) {
|
|
209
|
+
res.status(401).json({ error: 'unauthorized' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
(req as StorefrontRequest).user = user;
|
|
213
|
+
next();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
next(err);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toMcpAuthInfo(user: StorefrontUser): AuthInfo {
|
|
221
|
+
return {
|
|
222
|
+
token: user.accessToken,
|
|
223
|
+
clientId: user.buyerCustomerId,
|
|
224
|
+
scopes: user.scopes,
|
|
225
|
+
extra: {
|
|
226
|
+
buyerCustomerId: user.buyerCustomerId,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const transportLocks = new WeakMap<DecisioningAdcpServer, Promise<void>>();
|
|
232
|
+
|
|
233
|
+
async function handleWithPerRequestTransport(
|
|
234
|
+
server: DecisioningAdcpServer,
|
|
235
|
+
req: StorefrontRequest,
|
|
236
|
+
res: Response,
|
|
237
|
+
parsedBody: unknown
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const runTransportCycle = async (): Promise<void> => {
|
|
240
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
241
|
+
try {
|
|
242
|
+
await server.connect(transport);
|
|
243
|
+
if (parsedBody !== undefined) {
|
|
244
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
245
|
+
} else {
|
|
246
|
+
await transport.handleRequest(req, res);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('MCP transport error:', err);
|
|
250
|
+
if (!res.headersSent) {
|
|
251
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
await server.close();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const previous = transportLocks.get(server) ?? Promise.resolve();
|
|
259
|
+
const current = previous.then(runTransportCycle, runTransportCycle);
|
|
260
|
+
transportLocks.set(
|
|
261
|
+
server,
|
|
262
|
+
current.catch(() => {})
|
|
263
|
+
);
|
|
264
|
+
await current;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buyerCustomerIdFrom(ctx: ResolveContext | undefined): string | undefined {
|
|
268
|
+
const value = ctx?.authInfo?.extra?.buyerCustomerId;
|
|
269
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function accountIdFrom(ref: AccountReference | undefined): string | undefined {
|
|
273
|
+
return ref && 'account_id' in ref ? ref.account_id : undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function normalizePlatformId(value: string | undefined): PlatformId | null {
|
|
277
|
+
return value === 'snap' || value === 'vertex' ? value : null;
|
|
278
|
+
}
|