qontinui-types 0.6.0

Canonical DTO types for Qontinui. Rust is the source of truth; TypeScript and Python are generated from JSON Schema emitted by schemars.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! MCP (Model Context Protocol) client configuration DTOs.
//!
//! Wire-format types for the runner's MCP client subsystem: the transport
//! selector enum, the stdio/HTTP transport configs, the server config that
//! ties them together, the tool-info shapes returned by an MCP server, the
//! status / tool-call / DB-record payloads, and the create/update inputs
//! used by the CRUD Tauri commands and the `/mcp-servers` HTTP endpoints.
//!
//! These are ports of the shape-bearing portion of
//! `qontinui-runner/src-tauri/src/mcp_client/types.rs`. Behavior — the
//! `McpClientManager`, its `StdioHandle` / `McpConnection` runtime state,
//! the JSON-RPC 2.0 transport machinery, and the tool-calling methods — stays
//! in the runner. This module is data-only.
//!
//! ## Wire-format notes
//!
//! - `McpTransport` is a plain enum (not a tagged variant on `McpServerConfig`)
//!   because the persisted DB shape already has `transport` as a scalar column
//!   and `stdio_config` / `http_config` as separate nullable columns. Treating
//!   the transport as the tag of a sum over `StdioConfig` / `HttpConfig` would
//!   invert the existing wire contract. Callers check `config.transport` and
//!   then read the matching nested config struct.
//! - `McpTransport` serializes as lowercase (`"stdio"`, `"http"`) because the
//!   DB stores those exact strings; changing to snake_case would be a no-op
//!   here but `rename_all = "lowercase"` documents intent.
//! - `McpToolInfo.input_schema` uses the wire field name `"inputSchema"`
//!   (camelCase) because that's what the MCP spec mandates; every other field
//!   on this module follows the project-wide snake_case convention.
//! - Dates/times are ISO 8601 strings (see crate-level docs).
//!
//! ## Security
//!
//! Several fields on this module are **secret surfaces**:
//!
//! - `StdioConfig::command` and `StdioConfig::args` — arbitrary subprocess
//!   invocation. Any consumer that stores or surfaces these MUST treat them
//!   as code that will be executed on the runner host.
//! - `StdioConfig::env` — environment variables passed to the subprocess.
//!   Commonly holds API tokens, service account keys, etc.
//! - `HttpConfig::headers` — HTTP headers sent on every MCP request.
//!   Typically carries `Authorization: Bearer …` tokens or `X-Api-Key: …`
//!   values that must never leak to logs or UI-facing responses without
//!   redaction.
//!
//! These are persisted in the database (the whole point is reconstructing
//! a connection across runner restarts), so the at-rest shape includes them.
//! MCP / HTTP API responses that surface these configs to end-user UIs MUST
//! redact env values and auth-bearing headers before returning.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// ============================================================================
// Transport selector
// ============================================================================

/// Transport type for an MCP server connection.
///
/// Serialized as lowercase to match the existing DB column values
/// (`"stdio"` / `"http"`). Defaults to `Stdio` to preserve the pre-extraction
/// default from the runner.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum McpTransport {
    /// Stdio subprocess transport — launches the configured command and
    /// speaks newline-delimited JSON-RPC 2.0 over stdin/stdout.
    #[default]
    Stdio,
    /// HTTP transport — POSTs JSON-RPC 2.0 to the configured URL.
    Http,
}

// ============================================================================
// Transport configs
// ============================================================================

/// Stdio-transport configuration.
///
/// **Secret surface**: `command`, `args`, and `env` must be treated as
/// execution-critical. See module-level docs.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StdioConfig {
    /// Command to execute (e.g. `"npx"`, `"python"`, `"/usr/local/bin/server"`).
    #[serde(alias = "command")]
    pub command: String,
    /// Command arguments.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "args")]
    pub args: Vec<String>,
    /// Working directory (absolute path). `None` inherits the runner's cwd.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "cwd")]
    pub cwd: Option<String>,
    /// Extra environment variables for the subprocess. **Secret surface** —
    /// frequently holds API tokens.
    #[serde(default, skip_serializing_if = "HashMap::is_empty", alias = "env")]
    pub env: HashMap<String, String>,
}

/// HTTP-transport configuration.
///
/// **Secret surface**: `headers` typically carries an `Authorization` token.
/// See module-level docs.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct HttpConfig {
    /// Server URL (e.g. `"http://localhost:8080/mcp"`). The runner appends
    /// `/tools/list` and `/tools/call` to this base.
    #[serde(alias = "url")]
    pub url: String,
    /// HTTP headers to include on every request. **Secret surface** —
    /// typically includes `Authorization: Bearer …`.
    #[serde(default, skip_serializing_if = "HashMap::is_empty", alias = "headers")]
    pub headers: HashMap<String, String>,
}

// ============================================================================
// Serde default helpers (preserved from the runner so wire defaults match)
// ============================================================================

fn default_true() -> bool {
    true
}

fn default_timeout() -> u64 {
    30
}

// ============================================================================
// McpServerConfig
// ============================================================================

/// Full configuration for a registered MCP server.
///
/// Persisted in the `mcp_servers` Postgres table and surfaced to the frontend
/// through the `mcp_*` Tauri commands and the MCP `mcp-servers` HTTP
/// endpoint. The `transport` field selects which of `stdio_config` /
/// `http_config` is meaningful; the other is expected to be `None`.
///
/// **Secret surface**: the nested `stdio_config` / `http_config` carry secrets
/// — see their own docs and the module-level security note.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpServerConfig {
    /// Unique identifier (UUID).
    #[serde(alias = "id")]
    pub id: String,
    /// Display name.
    #[serde(alias = "name")]
    pub name: String,
    /// Optional human-readable description.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "description"
    )]
    pub description: Option<String>,

    /// Which transport this server uses.
    #[serde(alias = "transport")]
    pub transport: McpTransport,

    /// Stdio-transport settings. Expected to be `Some` iff
    /// `transport == McpTransport::Stdio`.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "stdio_config"
    )]
    pub stdio_config: Option<StdioConfig>,

    /// HTTP-transport settings. Expected to be `Some` iff
    /// `transport == McpTransport::Http`.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "http_config"
    )]
    pub http_config: Option<HttpConfig>,

    /// Whether this server is enabled. Disabled servers won't be connected
    /// even if `auto_start` is true. Default: `true`.
    #[serde(default = "default_true", alias = "enabled")]
    pub enabled: bool,

    /// Auto-connect when the runner launches. Default: `false`.
    #[serde(default, alias = "auto_start")]
    pub auto_start: bool,

    /// Per-request connection / tool-call timeout in seconds. Default: `30`.
    #[serde(default = "default_timeout", alias = "timeout_seconds")]
    pub timeout_seconds: u64,

    /// Serialized JSON list of tools cached from the last successful
    /// connection. Stored as a string for DB portability.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "cached_tools"
    )]
    pub cached_tools: Option<String>,

    /// ISO 8601 timestamp of when `cached_tools` was populated.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "tools_cached_at"
    )]
    pub tools_cached_at: Option<String>,

    /// ISO 8601 creation timestamp.
    #[serde(alias = "created_at")]
    pub created_at: String,

    /// ISO 8601 last-update timestamp.
    #[serde(alias = "updated_at")]
    pub updated_at: String,
}

// ============================================================================
// Tool schema / info
// ============================================================================

/// Input-parameter schema for an MCP tool.
///
/// Subset of JSON Schema — enough to render a form and validate arguments
/// before dispatching a `tools/call`. The `properties` and `required` fields
/// are passed through verbatim from the server.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpToolInputSchema {
    /// JSON Schema `type` (typically `"object"`).
    #[serde(rename = "type", alias = "schema_type")]
    pub schema_type: String,
    /// Optional human-readable description.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "description"
    )]
    pub description: Option<String>,
    /// JSON-Schema-shaped property descriptors, kept as opaque JSON.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "properties")]
    pub properties: Option<serde_json::Value>,
    /// Names of required properties.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "required")]
    pub required: Option<Vec<String>>,
}

/// Single tool exposed by an MCP server, as returned by `tools/list`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpToolInfo {
    /// Tool name (the argument passed back on `tools/call`).
    #[serde(alias = "name")]
    pub name: String,
    /// Tool description shown to users.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "description"
    )]
    pub description: Option<String>,
    /// Input-argument schema. Wire field is `"inputSchema"` per MCP spec.
    #[serde(alias = "input_schema")]
    pub input_schema: McpToolInputSchema,
}

// ============================================================================
// Server status
// ============================================================================

/// Status of a single MCP server, as reported by the client manager.
///
/// Derived from runtime connection state; never persisted. `tools` is
/// populated only when `connected == true`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpServerStatus {
    /// ID of the server this status refers to.
    #[serde(alias = "server_id")]
    pub server_id: String,
    /// Whether the client currently holds a live connection.
    #[serde(alias = "connected")]
    pub connected: bool,
    /// Last connection / tool-call error, if any.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "error")]
    pub error: Option<String>,
    /// Available tools — `Some(…)` when connected, `None` otherwise.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "tools")]
    pub tools: Option<Vec<McpToolInfo>>,
    /// ISO 8601 timestamp of the most recent connection attempt.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "last_connect_attempt"
    )]
    pub last_connect_attempt: Option<String>,
    /// ISO 8601 timestamp of the most recent successful connection.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "last_connected"
    )]
    pub last_connected: Option<String>,
}

// ============================================================================
// Create / update inputs
// ============================================================================

/// Request body for creating a new MCP server configuration.
///
/// **Secret surface**: as with [`McpServerConfig`], the nested transport
/// configs carry secrets.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct CreateMcpServerInput {
    /// Display name.
    #[serde(alias = "name")]
    pub name: String,
    /// Optional description.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "description"
    )]
    pub description: Option<String>,
    /// Transport selector.
    #[serde(alias = "transport")]
    pub transport: McpTransport,
    /// Stdio config (required when `transport == Stdio`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "stdio_config"
    )]
    pub stdio_config: Option<StdioConfig>,
    /// HTTP config (required when `transport == Http`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "http_config"
    )]
    pub http_config: Option<HttpConfig>,
    /// Override for the default `enabled = true`.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "enabled")]
    pub enabled: Option<bool>,
    /// Override for the default `auto_start = false`.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "auto_start")]
    pub auto_start: Option<bool>,
    /// Override for the default `timeout_seconds = 30`.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "timeout_seconds"
    )]
    pub timeout_seconds: Option<u64>,
}

/// Request body for updating an MCP server configuration. Every field is
/// optional — fields left as `None` are preserved from the existing row.
///
/// **Secret surface**: as with [`McpServerConfig`], the nested transport
/// configs carry secrets.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UpdateMcpServerInput {
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "name")]
    pub name: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "description"
    )]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "transport")]
    pub transport: Option<McpTransport>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "stdio_config"
    )]
    pub stdio_config: Option<StdioConfig>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "http_config"
    )]
    pub http_config: Option<HttpConfig>,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "enabled")]
    pub enabled: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "auto_start")]
    pub auto_start: Option<bool>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "timeout_seconds"
    )]
    pub timeout_seconds: Option<u64>,
}

// ============================================================================
// Tool-call result
// ============================================================================

/// Result of a single `tools/call` invocation.
///
/// Shape is always the same regardless of success / failure: check `success`
/// first, then read `content` (on success) or `error` (on failure).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpToolCallResult {
    /// Whether the call succeeded.
    #[serde(alias = "success")]
    pub success: bool,
    /// Response content (present on success). Usually a JSON object/array,
    /// but can be a primitive if the tool returned text-only content.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "content")]
    pub content: Option<serde_json::Value>,
    /// Error message (present on failure).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "error")]
    pub error: Option<String>,
    /// Response type tag: `"json"`, `"text"`, or `"error"`.
    #[serde(alias = "response_type")]
    pub response_type: String,
    /// Wall-clock duration of the call in milliseconds.
    #[serde(alias = "duration_ms")]
    pub duration_ms: u64,
}

// ============================================================================
// Call-history DB types
// ============================================================================

/// Input shape for recording an MCP call to the `mcp_calls` table.
///
/// `arguments` / `resolved_arguments` / `response` / `extractions` /
/// `assertions` are serialized-JSON strings rather than `serde_json::Value`
/// because the DB layer stores them as `TEXT` / `JSONB` strings and does not
/// round-trip through a `Value`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct CreateMcpCallInput {
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    #[serde(alias = "step_id")]
    pub step_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "step_name")]
    pub step_name: Option<String>,
    #[serde(alias = "server_id")]
    pub server_id: String,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "server_name"
    )]
    pub server_name: Option<String>,
    #[serde(alias = "tool_name")]
    pub tool_name: String,
    /// JSON-serialized arguments as originally submitted.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "arguments")]
    pub arguments: Option<String>,
    /// JSON-serialized arguments after variable resolution.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "resolved_arguments"
    )]
    pub resolved_arguments: Option<String>,
    /// JSON-serialized response body.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "response")]
    pub response: Option<String>,
    /// Response type tag (see [`McpToolCallResult::response_type`]).
    #[serde(alias = "response_type")]
    pub response_type: String,
    /// Wall-clock duration in milliseconds.
    #[serde(alias = "duration_ms")]
    pub duration_ms: i64,
    /// JSON-serialized extractions (variables pulled from the response).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "extractions"
    )]
    pub extractions: Option<String>,
    /// JSON-serialized assertion results.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "assertions")]
    pub assertions: Option<String>,
    /// Whether the call succeeded.
    #[serde(alias = "success")]
    pub success: bool,
    /// Error message if the call failed.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "error_message"
    )]
    pub error_message: Option<String>,
}

/// Persisted MCP call record as read back from the `mcp_calls` table.
///
/// Same shape as [`CreateMcpCallInput`] plus the row id and creation
/// timestamp.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpCallRecord {
    #[serde(alias = "id")]
    pub id: String,
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    #[serde(alias = "step_id")]
    pub step_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "step_name")]
    pub step_name: Option<String>,
    #[serde(alias = "server_id")]
    pub server_id: String,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "server_name"
    )]
    pub server_name: Option<String>,
    #[serde(alias = "tool_name")]
    pub tool_name: String,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "arguments")]
    pub arguments: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "resolved_arguments"
    )]
    pub resolved_arguments: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "response")]
    pub response: Option<String>,
    #[serde(alias = "response_type")]
    pub response_type: String,
    #[serde(alias = "duration_ms")]
    pub duration_ms: i64,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "extractions"
    )]
    pub extractions: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "assertions")]
    pub assertions: Option<String>,
    #[serde(alias = "success")]
    pub success: bool,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "error_message"
    )]
    pub error_message: Option<String>,
    /// ISO 8601 creation timestamp.
    #[serde(alias = "created_at")]
    pub created_at: String,
}

/// Result envelope for an `mcp_calls` query scoped to a single task run.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct McpCallsResult {
    /// Task run the query was scoped to.
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    /// Matched call records (paginated; see `total_count` for the full size).
    #[serde(alias = "calls")]
    pub calls: Vec<McpCallRecord>,
    /// Number of calls returned in `calls`.
    #[serde(alias = "count")]
    pub count: usize,
    /// Total matching rows across all pages. Defaults to `0` on older rows
    /// that predate the field; consumers should prefer `count` when this is
    /// absent.
    #[serde(default, alias = "total_count")]
    pub total_count: usize,
    /// Number of successful calls in the current page.
    #[serde(alias = "success_count")]
    pub success_count: usize,
    /// Number of failed calls in the current page.
    #[serde(alias = "failed_count")]
    pub failed_count: usize,
    /// Whether there are more rows after the current page.
    #[serde(default, alias = "has_more")]
    pub has_more: bool,
}