@narrative.io/data-collaboration-sdk-ts 4.0.0 → 4.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.
@@ -41,6 +41,27 @@ export interface ToolSpec {
41
41
  /** Defaults to true. Leave true in production. */
42
42
  strict?: boolean;
43
43
  }
44
+ /** How hard a model should think, for models that take an effort scale. */
45
+ export type ThinkingEffortLevel = "low" | "medium" | "high";
46
+ /**
47
+ * What a model should do about thinking. Models differ in which form they take:
48
+ * one accepts an effort level, another a token budget, some both. A setting is
49
+ * only valid against the model it is sent to, and one the model cannot honour is
50
+ * a 400 rather than a silent downgrade.
51
+ *
52
+ * Omit the field entirely to get the model's own behaviour.
53
+ *
54
+ * Reasoning comes back as `reasoning` content blocks on the assistant turn,
55
+ * ahead of the answer. Some models return their working as words; others return
56
+ * only a signature.
57
+ */
58
+ export type ThinkingSetting = "disabled" | {
59
+ type: "effort";
60
+ level: ThinkingEffortLevel;
61
+ } | {
62
+ type: "budget";
63
+ budget_tokens: number;
64
+ };
44
65
  export interface ConversationDefaults {
45
66
  model: AgentModel;
46
67
  data_plane_id: string;
@@ -49,6 +70,18 @@ export interface ConversationDefaults {
49
70
  max_iterations?: number;
50
71
  max_tokens?: number;
51
72
  temperature?: number;
73
+ /**
74
+ * Whether the model should reason before answering, and how hard.
75
+ *
76
+ * A token budget must be at least 1024 and smaller than `max_tokens`:
77
+ * thinking is spent from the same output budget as the answer, so a budget
78
+ * that fills it leaves nothing to answer with.
79
+ *
80
+ * Cannot be combined with `tool_choice: { kind: "specific_tool" }` on a run.
81
+ * A forced tool stops the model reasoning, so that combination is refused
82
+ * rather than half-honoured.
83
+ */
84
+ thinking?: ThinkingSetting;
52
85
  output_format_schema?: JsonSchemaObject;
53
86
  mcp_servers?: McpServerConfig[];
54
87
  tools?: ToolSpec[];
@@ -104,6 +137,8 @@ export interface RunConfigOverride {
104
137
  max_iterations?: number;
105
138
  max_tokens?: number;
106
139
  temperature?: number;
140
+ /** Shadows the conversation's setting for this run only. See {@link ConversationDefaults.thinking}. */
141
+ thinking?: ThinkingSetting;
107
142
  output_format_schema?: JsonSchemaObject;
108
143
  mcp_servers?: McpServerConfig[];
109
144
  tools?: ToolSpec[];
@@ -149,6 +184,11 @@ export interface LiveRunTurn {
149
184
  turn_index: number;
150
185
  role: AgentMessageRole;
151
186
  content_blocks: ContentBlock[];
187
+ /**
188
+ * Same meaning as on a committed message, but empty while the turn is still
189
+ * streaming: the answer is resolved when the run finalizes.
190
+ */
191
+ final_answer_from?: BlockId[];
152
192
  }
153
193
  /**
154
194
  * The conversation's current, still-mutating state. Always present on a run
@@ -202,9 +242,17 @@ export interface RunResponse {
202
242
  live: RunLiveProgress;
203
243
  }
204
244
  export type AgentMessageRole = "user" | "assistant" | "tool";
245
+ /**
246
+ * Identifier a block was produced under, e.g. `b_c08cf800_02` — the first eight
247
+ * characters of the inference job's id and the block's position in that
248
+ * response. Present on blocks the platform produced; absent on blocks a caller
249
+ * sent, and on turns stored before ids existed.
250
+ */
251
+ export type BlockId = string;
205
252
  export interface TextContentBlock {
206
253
  type: "text";
207
254
  text: string;
255
+ id?: BlockId;
208
256
  }
209
257
  export interface ToolUseContentBlock {
210
258
  type: "tool_use";
@@ -212,14 +260,43 @@ export interface ToolUseContentBlock {
212
260
  /** Fully-aliased wire name (`{alias}-{tool_name}` for MCP, bare for caller-declared). */
213
261
  name: string;
214
262
  arguments: Record<string, unknown>;
263
+ id?: BlockId;
215
264
  }
216
265
  export interface ToolResultContentBlock {
217
266
  type: "tool_result";
218
267
  tool_use_id: ToolUseId;
219
268
  content: ContentBlock[];
220
269
  is_error: boolean;
270
+ id?: BlockId;
271
+ }
272
+ /**
273
+ * The model's own working, on an assistant turn where the run asked for
274
+ * thinking. Comes first in the turn, ahead of the answer.
275
+ *
276
+ * `text` is optional and often absent: some models return their reasoning as
277
+ * words, others return only a `signature` and keep the words to themselves. A
278
+ * block with no `text` still means the model reasoned — and that those tokens
279
+ * were billed — so don't read its absence as "nothing happened".
280
+ *
281
+ * `signature` is issued by the provider and opaque: not a hash you can verify
282
+ * or a token you can decode. Send the block back unchanged to continue a turn
283
+ * as the model left it, or drop it — a continuation without reasoning blocks is
284
+ * accepted.
285
+ */
286
+ export interface ReasoningContentBlock {
287
+ type: "reasoning";
288
+ text?: string;
289
+ signature: string;
290
+ id?: BlockId;
221
291
  }
222
- export type ContentBlock = TextContentBlock | ToolUseContentBlock | ToolResultContentBlock;
292
+ /** Reasoning the provider encrypted rather than returning. Nothing to display; kept so a turn can be replayed whole. */
293
+ export interface RedactedReasoningContentBlock {
294
+ type: "redacted_reasoning";
295
+ /** Base64 of the encrypted reasoning. */
296
+ data: string;
297
+ id?: BlockId;
298
+ }
299
+ export type ContentBlock = TextContentBlock | ToolUseContentBlock | ToolResultContentBlock | ReasoningContentBlock | RedactedReasoningContentBlock;
223
300
  export interface MessageDto {
224
301
  id: string;
225
302
  sequence_no: number;
@@ -227,6 +304,18 @@ export interface MessageDto {
227
304
  content_blocks: ContentBlock[];
228
305
  run_id: RunId;
229
306
  created_at: string;
307
+ /**
308
+ * Ids of the blocks this turn's answer was taken from — always text blocks
309
+ * that are present in `content_blocks`, and empty on a turn that answered
310
+ * nothing (a tool-call turn, a user's message, a turn still streaming).
311
+ *
312
+ * Render the reply from these rather than from every text block: with
313
+ * thinking on, a model may narrate its working as ordinary text before
314
+ * answering, and that narration is persisted deliberately. Use
315
+ * {@link answerTextBlocks}, which also handles turns stored before ids
316
+ * existed.
317
+ */
318
+ final_answer_from?: BlockId[];
230
319
  }
231
320
  export interface ListMessagesResponse {
232
321
  current_version: ConversationVersion;
@@ -243,3 +332,29 @@ export interface AgentRfcError {
243
332
  }
244
333
  export declare const TERMINAL_RUN_STATUSES: ReadonlyArray<RunStatus>;
245
334
  export declare function isTerminalRunStatus(status: RunStatus): boolean;
335
+ /** Narrows to a reasoning block, whose `text` may still be absent. */
336
+ export declare function isReasoningBlock(block: ContentBlock): block is ReasoningContentBlock;
337
+ /**
338
+ * The text blocks of a turn that make up its reply — what to render as the
339
+ * assistant's message.
340
+ *
341
+ * Two shapes of turn exist and both are live, so which blocks count depends on
342
+ * whether the platform produced this turn:
343
+ *
344
+ * - **A turn with block ids** is one we produced. Its reply is exactly the
345
+ * blocks named by `final_answer_from`, which is empty while the turn is
346
+ * still streaming and on turns that answer nothing (a tool-call turn). Empty
347
+ * means *render nothing yet* — not "fall back to everything" — because such
348
+ * a turn may also hold the model's working as ordinary text, and that
349
+ * narration is not the reply.
350
+ * - **A turn without block ids** is a caller's message or a turn stored before
351
+ * ids existed. There is no pointer to consult, so its reply is every text
352
+ * block, which is how these turns have always rendered.
353
+ *
354
+ * Ids the pointer names but the turn doesn't contain are ignored rather than
355
+ * trusted, so a malformed pointer renders nothing instead of throwing.
356
+ */
357
+ export declare function answerTextBlocks(turn: {
358
+ content_blocks: ContentBlock[];
359
+ final_answer_from?: BlockId[];
360
+ }): TextContentBlock[];
@@ -20,3 +20,36 @@ export const TERMINAL_RUN_STATUSES = [
20
20
  export function isTerminalRunStatus(status) {
21
21
  return TERMINAL_RUN_STATUSES.includes(status);
22
22
  }
23
+ // ---------- Reading a turn ----------
24
+ /** Narrows to a reasoning block, whose `text` may still be absent. */
25
+ export function isReasoningBlock(block) {
26
+ return block.type === "reasoning";
27
+ }
28
+ /**
29
+ * The text blocks of a turn that make up its reply — what to render as the
30
+ * assistant's message.
31
+ *
32
+ * Two shapes of turn exist and both are live, so which blocks count depends on
33
+ * whether the platform produced this turn:
34
+ *
35
+ * - **A turn with block ids** is one we produced. Its reply is exactly the
36
+ * blocks named by `final_answer_from`, which is empty while the turn is
37
+ * still streaming and on turns that answer nothing (a tool-call turn). Empty
38
+ * means *render nothing yet* — not "fall back to everything" — because such
39
+ * a turn may also hold the model's working as ordinary text, and that
40
+ * narration is not the reply.
41
+ * - **A turn without block ids** is a caller's message or a turn stored before
42
+ * ids existed. There is no pointer to consult, so its reply is every text
43
+ * block, which is how these turns have always rendered.
44
+ *
45
+ * Ids the pointer names but the turn doesn't contain are ignored rather than
46
+ * trusted, so a malformed pointer renders nothing instead of throwing.
47
+ */
48
+ export function answerTextBlocks(turn) {
49
+ const textBlocks = turn.content_blocks.filter((b) => b.type === "text");
50
+ const produced = turn.content_blocks.some((b) => b.id !== undefined);
51
+ if (!produced)
52
+ return textBlocks;
53
+ const named = new Set(turn.final_answer_from ?? []);
54
+ return textBlocks.filter((b) => b.id !== undefined && named.has(b.id));
55
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Type-level coverage for `src/agents/types.ts`. These assertions are checked by
3
+ * `tsc` (the file is picked up by tsconfig `include` and is not matched by the
4
+ * `**\/*.test.ts` exclude), so a broken assertion is a compile error at build
5
+ * time — jest never runs this file. Runtime helpers (`isTerminalRunStatus`,
6
+ * `TERMINAL_RUN_STATUSES`) are covered separately in `tests/Agents.test.ts`.
7
+ */
8
+ import type { AgentModel, ContentBlock, ConversationDefaults, CreateConversationRequest, CreateRunRequest, LiveRunTurn, MessageDto, ReasoningContentBlock, RedactedReasoningContentBlock, RunConfigOverride, RunPayload, TextContentBlock, ToolChoice, ToolResultContentBlock, ToolUseContentBlock } from "./types";
9
+ type IsAssignable<T, U> = T extends U ? true : false;
10
+ type AssertTrue<T extends true> = T;
11
+ type AssertFalse<T extends false> = T;
12
+ type IsDeclaredModel<M extends string> = [Extract<AgentModel, M>] extends [
13
+ never
14
+ ] ? false : true;
15
+ export type HasSonnet50 = AssertTrue<IsDeclaredModel<"anthropic.claude-sonnet-5.0">>;
16
+ export type HasOpus47 = AssertTrue<IsDeclaredModel<"anthropic.claude-opus-4.7">>;
17
+ export type HasOpus48 = AssertTrue<IsDeclaredModel<"anthropic.claude-opus-4.8">>;
18
+ export type HasSonnet46 = AssertTrue<IsDeclaredModel<"anthropic.claude-sonnet-4.6">>;
19
+ export type HasHaiku45 = AssertTrue<IsDeclaredModel<"anthropic.claude-haiku-4.5">>;
20
+ export type HasGpt41 = AssertTrue<IsDeclaredModel<"openai.gpt-4.1">>;
21
+ export type RejectsUndeclaredModel = AssertFalse<IsDeclaredModel<"anthropic.claude-opus-9.9">>;
22
+ export type RejectsMisspelledModel = AssertFalse<IsDeclaredModel<"anthropic.claude-sonet-5.0">>;
23
+ export type UserMessageIsPayload = AssertTrue<IsAssignable<{
24
+ kind: "user_message";
25
+ text: "hi";
26
+ }, RunPayload>>;
27
+ export type ToolOutputsIsPayload = AssertTrue<IsAssignable<{
28
+ kind: "tool_outputs";
29
+ outputs: [{
30
+ tool_use_id: "t";
31
+ content: "ok";
32
+ }];
33
+ }, RunPayload>>;
34
+ export type UnknownKindIsNotPayload = AssertFalse<IsAssignable<{
35
+ kind: "assistant_message";
36
+ text: "hi";
37
+ }, RunPayload>>;
38
+ export type UserMessageNeedsText = AssertFalse<IsAssignable<{
39
+ kind: "user_message";
40
+ }, RunPayload>>;
41
+ export type AutoIsToolChoice = AssertTrue<IsAssignable<{
42
+ kind: "auto";
43
+ }, ToolChoice>>;
44
+ export type AnyIsToolChoice = AssertTrue<IsAssignable<{
45
+ kind: "any";
46
+ }, ToolChoice>>;
47
+ export type SpecificToolIsToolChoice = AssertTrue<IsAssignable<{
48
+ kind: "specific_tool";
49
+ name: "lookup";
50
+ }, ToolChoice>>;
51
+ export type SpecificToolWithAlias = AssertTrue<IsAssignable<{
52
+ kind: "specific_tool";
53
+ name: "lookup";
54
+ mcp_alias: "gh";
55
+ }, ToolChoice>>;
56
+ export type SpecificToolNeedsName = AssertFalse<IsAssignable<{
57
+ kind: "specific_tool";
58
+ }, ToolChoice>>;
59
+ export type TextIsContentBlock = AssertTrue<IsAssignable<{
60
+ type: "text";
61
+ text: "hello";
62
+ }, ContentBlock>>;
63
+ export type UnknownTypeIsNotContentBlock = AssertFalse<IsAssignable<{
64
+ type: "image";
65
+ url: "x";
66
+ }, ContentBlock>>;
67
+ type NarrowText = Extract<ContentBlock, {
68
+ type: "text";
69
+ }>;
70
+ export type NarrowTextToBlock = AssertTrue<IsAssignable<NarrowText, TextContentBlock>>;
71
+ export type NarrowBlockToText = AssertTrue<IsAssignable<TextContentBlock, NarrowText>>;
72
+ type NarrowToolUse = Extract<ContentBlock, {
73
+ type: "tool_use";
74
+ }>;
75
+ export type NarrowsToToolUseBlock = AssertTrue<IsAssignable<NarrowToolUse, ToolUseContentBlock>>;
76
+ type NarrowToolResult = Extract<ContentBlock, {
77
+ type: "tool_result";
78
+ }>;
79
+ export type NarrowsToToolResultBlock = AssertTrue<IsAssignable<NarrowToolResult, ToolResultContentBlock>>;
80
+ export type CreateConversationNeedsDefaults = AssertFalse<IsAssignable<{
81
+ name: "c";
82
+ }, CreateConversationRequest>>;
83
+ export type MinimalCreateConversation = AssertTrue<IsAssignable<{
84
+ defaults: {
85
+ model: "anthropic.claude-opus-4.8";
86
+ data_plane_id: "dp";
87
+ execution_cluster: "shared";
88
+ };
89
+ }, CreateConversationRequest>>;
90
+ export type DefaultsNeedModel = AssertFalse<IsAssignable<{
91
+ data_plane_id: "dp";
92
+ execution_cluster: "shared";
93
+ }, ConversationDefaults>>;
94
+ export type BadExecutionCluster = AssertFalse<IsAssignable<{
95
+ model: "anthropic.claude-opus-4.8";
96
+ data_plane_id: "dp";
97
+ execution_cluster: "gpu";
98
+ }, ConversationDefaults>>;
99
+ type RequiredDefaults = {
100
+ model: "anthropic.claude-opus-5.0";
101
+ data_plane_id: "dp";
102
+ execution_cluster: "shared";
103
+ };
104
+ type Defaults<T> = RequiredDefaults & T;
105
+ export type ThinkingIsOptional = AssertTrue<IsAssignable<RequiredDefaults, ConversationDefaults>>;
106
+ export type DisabledIsASetting = AssertTrue<IsAssignable<Defaults<{
107
+ thinking: "disabled";
108
+ }>, ConversationDefaults>>;
109
+ export type EffortIsASetting = AssertTrue<IsAssignable<Defaults<{
110
+ thinking: {
111
+ type: "effort";
112
+ level: "high";
113
+ };
114
+ }>, ConversationDefaults>>;
115
+ export type BudgetIsASetting = AssertTrue<IsAssignable<Defaults<{
116
+ thinking: {
117
+ type: "budget";
118
+ budget_tokens: 4096;
119
+ };
120
+ }>, ConversationDefaults>>;
121
+ export type BadEffortLevelRejected = AssertFalse<IsAssignable<Defaults<{
122
+ thinking: {
123
+ type: "effort";
124
+ level: "extreme";
125
+ };
126
+ }>, ConversationDefaults>>;
127
+ export type UnknownVocabularyRejected = AssertFalse<IsAssignable<Defaults<{
128
+ thinking: {
129
+ type: "telepathy";
130
+ };
131
+ }>, ConversationDefaults>>;
132
+ export type BareStringRejected = AssertFalse<IsAssignable<Defaults<{
133
+ thinking: "high";
134
+ }>, ConversationDefaults>>;
135
+ export type BudgetWithoutTokensRejected = AssertFalse<IsAssignable<Defaults<{
136
+ thinking: {
137
+ type: "budget";
138
+ };
139
+ }>, ConversationDefaults>>;
140
+ export type EffortWithoutLevelRejected = AssertFalse<IsAssignable<Defaults<{
141
+ thinking: {
142
+ type: "effort";
143
+ };
144
+ }>, ConversationDefaults>>;
145
+ export type OverrideTakesThinking = AssertTrue<IsAssignable<{
146
+ thinking: {
147
+ type: "effort";
148
+ level: "low";
149
+ };
150
+ }, RunConfigOverride>>;
151
+ export type OverrideRejectsBadThinking = AssertFalse<IsAssignable<{
152
+ thinking: {
153
+ type: "effort";
154
+ level: "maximum";
155
+ };
156
+ }, RunConfigOverride>>;
157
+ export type MinimalCreateRun = AssertTrue<IsAssignable<{
158
+ client_op_id: "op";
159
+ expected_version: 0;
160
+ payload: {
161
+ kind: "user_message";
162
+ text: "hi";
163
+ };
164
+ }, CreateRunRequest>>;
165
+ export type CreateRunNeedsPayload = AssertFalse<IsAssignable<{
166
+ client_op_id: "op";
167
+ expected_version: 0;
168
+ }, CreateRunRequest>>;
169
+ export type LegacyTextBlockStillAssignable = AssertTrue<IsAssignable<{
170
+ type: "text";
171
+ text: "hi";
172
+ }, ContentBlock>>;
173
+ export type IdentifiedTextBlockAssignable = AssertTrue<IsAssignable<{
174
+ type: "text";
175
+ text: "hi";
176
+ id: "b_c08cf800_01";
177
+ }, ContentBlock>>;
178
+ export type ReasoningWithoutTextAssignable = AssertTrue<IsAssignable<{
179
+ type: "reasoning";
180
+ signature: "c2ln";
181
+ }, ReasoningContentBlock>>;
182
+ export type ReasoningWithTextAssignable = AssertTrue<IsAssignable<{
183
+ type: "reasoning";
184
+ signature: "c2ln";
185
+ text: "working it out";
186
+ }, ReasoningContentBlock>>;
187
+ export type ReasoningWithoutSignatureRejected = AssertFalse<IsAssignable<{
188
+ type: "reasoning";
189
+ text: "no signature";
190
+ }, ReasoningContentBlock>>;
191
+ export type ReasoningIsAContentBlock = AssertTrue<IsAssignable<ReasoningContentBlock, ContentBlock>>;
192
+ export type RedactedReasoningIsAContentBlock = AssertTrue<IsAssignable<RedactedReasoningContentBlock, ContentBlock>>;
193
+ export type UnknownBlockTypeRejected = AssertFalse<IsAssignable<{
194
+ type: "vibes";
195
+ }, ContentBlock>>;
196
+ export type LegacyMessageStillAssignable = AssertTrue<IsAssignable<{
197
+ id: "m1";
198
+ sequence_no: 1;
199
+ role: "assistant";
200
+ content_blocks: [{
201
+ type: "text";
202
+ text: "4";
203
+ }];
204
+ run_id: "r1";
205
+ created_at: "2026-07-30T00:00:00Z";
206
+ }, MessageDto>>;
207
+ export type MessageWithPointerAssignable = AssertTrue<IsAssignable<{
208
+ id: "m1";
209
+ sequence_no: 2;
210
+ role: "assistant";
211
+ content_blocks: [
212
+ {
213
+ type: "reasoning";
214
+ signature: "c2ln";
215
+ id: "b_1_00";
216
+ },
217
+ {
218
+ type: "text";
219
+ text: "{}";
220
+ id: "b_1_01";
221
+ }
222
+ ];
223
+ run_id: "r1";
224
+ created_at: "2026-07-30T00:00:00Z";
225
+ final_answer_from: ["b_1_01"];
226
+ }, MessageDto>>;
227
+ export type LegacyLiveTurnStillAssignable = AssertTrue<IsAssignable<{
228
+ turn_index: 0;
229
+ role: "assistant";
230
+ content_blocks: [];
231
+ }, LiveRunTurn>>;
232
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -6,7 +6,10 @@ import type { CreateMcpConnectionRequest, ListMcpConnectionsResponse, McpConnect
6
6
  * can reference it by id via `mcp_servers[].connection_id`. Tokens are never returned by the API.
7
7
  *
8
8
  * The OAuth callback (`GET /mcp-connections/callback`) is a browser redirect, not an SDK call, so
9
- * it is intentionally not wrapped here.
9
+ * it is intentionally not wrapped here. It only stores the authorization code and redirects; the
10
+ * app then calls {@link McpConnectionsApi.completeMcpConnection} to finish, authenticated as the
11
+ * user who began the flow. Because the callback is public, it is not allowed to complete anything
12
+ * itself — otherwise a `state` alone could turn another user's consent into a stored token.
10
13
  */
11
14
  export declare class McpConnectionsApi extends BaseApi {
12
15
  /**
@@ -21,6 +24,20 @@ export declare class McpConnectionsApi extends BaseApi {
21
24
  * `authorization_url` the user must visit to consent.
22
25
  */
23
26
  createMcpConnection(request: CreateMcpConnectionRequest): Promise<McpConnectionCreatedResponse>;
27
+ /**
28
+ * Completes a connection begun with {@link McpConnectionsApi.createMcpConnection}, once the user
29
+ * has consented and the OAuth callback has stored the authorization code. Exchanges that code and
30
+ * validates the connection with a `tools/list` call before storing the tokens.
31
+ *
32
+ * Must be called with the credentials of the user who began the flow — the `state` on its own is
33
+ * not enough.
34
+ * @param {string} state - The one-time `state` the OAuth callback forwarded to the app.
35
+ * @returns {Promise<McpConnectionSummary>} The connection's status view, normally `connected`.
36
+ * Rejects with 404 if the connection does not exist or belongs to another user; 400 if the
37
+ * `state` is unknown or expired, the authorization code has not arrived yet, or the token
38
+ * exchange or validation failed.
39
+ */
40
+ completeMcpConnection(state: string): Promise<McpConnectionSummary>;
24
41
  /**
25
42
  * Fetches one of the calling user's connections.
26
43
  * @param {string} connectionId - The connection id.
@@ -6,7 +6,10 @@ const resourceName = "mcp-connections";
6
6
  * can reference it by id via `mcp_servers[].connection_id`. Tokens are never returned by the API.
7
7
  *
8
8
  * The OAuth callback (`GET /mcp-connections/callback`) is a browser redirect, not an SDK call, so
9
- * it is intentionally not wrapped here.
9
+ * it is intentionally not wrapped here. It only stores the authorization code and redirects; the
10
+ * app then calls {@link McpConnectionsApi.completeMcpConnection} to finish, authenticated as the
11
+ * user who began the flow. Because the callback is public, it is not allowed to complete anything
12
+ * itself — otherwise a `state` alone could turn another user's consent into a stored token.
10
13
  */
11
14
  export class McpConnectionsApi extends BaseApi {
12
15
  /**
@@ -25,6 +28,22 @@ export class McpConnectionsApi extends BaseApi {
25
28
  async createMcpConnection(request) {
26
29
  return await this.post(resourceName, request);
27
30
  }
31
+ /**
32
+ * Completes a connection begun with {@link McpConnectionsApi.createMcpConnection}, once the user
33
+ * has consented and the OAuth callback has stored the authorization code. Exchanges that code and
34
+ * validates the connection with a `tools/list` call before storing the tokens.
35
+ *
36
+ * Must be called with the credentials of the user who began the flow — the `state` on its own is
37
+ * not enough.
38
+ * @param {string} state - The one-time `state` the OAuth callback forwarded to the app.
39
+ * @returns {Promise<McpConnectionSummary>} The connection's status view, normally `connected`.
40
+ * Rejects with 404 if the connection does not exist or belongs to another user; 400 if the
41
+ * `state` is unknown or expired, the authorization code has not arrived yet, or the token
42
+ * exchange or validation failed.
43
+ */
44
+ async completeMcpConnection(state) {
45
+ return await this.post(`${resourceName}/complete`, { state });
46
+ }
28
47
  /**
29
48
  * Fetches one of the calling user's connections.
30
49
  * @param {string} connectionId - The connection id.
@@ -7,6 +7,9 @@ export interface McpConnectionCreatedResponse {
7
7
  connection_id: string;
8
8
  authorization_url: string;
9
9
  }
10
+ export interface CompleteMcpConnectionRequest {
11
+ state: string;
12
+ }
10
13
  export interface McpConnectionSummary {
11
14
  connection_id: string;
12
15
  server_url: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/data-collaboration-sdk-ts",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "main": "build/index.js",
5
5
  "repository": "github:narrative-io/data-collaboration-sdk-ts",
6
6
  "source": "src/index.ts",