Files
oai-swift/oAITests/ExternalMCPManagerConversionTests.swift
T
rune 25028e3405 Add env-var support and native HTTP transport for External MCP Servers
External MCP Servers previously only spoke stdio (spawn a local
command + args). Adds:
- env vars for stdio servers (merged into the subprocess environment,
  not embedded in the args string), with a masked key-value editor
- a native Streamable HTTP transport (URL + Bearer token + custom
  headers), so HTTP-based MCP servers like Obsidian's Local REST API
  plugin connect directly without needing npx/Node.js as a bridge

Introduces an MCPTransport abstraction (stdio/HTTP) so ExternalMCPClient
stays transport-agnostic — mirrors how Provider.swift already abstracts
AI backends in this codebase.

Also fixes a real crash found via live testing against Obsidian:
convertInputSchema force-unwrapped a tool parameter's `type`, which
isn't required by JSON Schema — Obsidian's plugin was the first real
server to send a parameter without one. Live-verified end to end
(vault search/read/write/edit) before this commit, per the project's
standing rule to hold external-service-dependent changes until they're
actually confirmed working, not just compiling and passing tests.
2026-08-13 16:05:42 +02:00

66 lines
2.7 KiB
Swift

//
// ExternalMCPManagerConversionTests.swift
// ConfabTests
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
import Testing
import Foundation
@testable import Confab
@Suite("ExternalMCPManager.convertInputSchema")
struct ExternalMCPManagerConversionTests {
/// Regression test for a real crash: Obsidian's Local REST API plugin's MCP tool schemas
/// include at least one property with no "type" key at all — valid JSON Schema (e.g. an
/// enum-only or composed property) — which a `prop.type!` force-unwrap used to crash on the
/// moment a live HTTP MCP server's tools/list response reached this code.
@Test("A property with no type at all defaults to string instead of crashing")
func propertyWithNoTypeDefaultsToString() {
let prop = MCPPropertySchema(type: nil, description: "no explicit type", enum: nil, items: nil)
let schema = MCPInputSchema(type: "object", properties: ["mode": prop], required: nil)
let parameters = ExternalMCPManager.convertInputSchema(schema)
#expect(parameters.properties["mode"]?.type == "string")
#expect(parameters.properties["mode"]?.description == "no explicit type")
}
@Test("integer is normalized to number")
func integerNormalizesToNumber() {
let prop = MCPPropertySchema(type: "integer", description: nil, enum: nil, items: nil)
let schema = MCPInputSchema(type: "object", properties: ["count": prop], required: nil)
let parameters = ExternalMCPManager.convertInputSchema(schema)
#expect(parameters.properties["count"]?.type == "number")
}
@Test("An unrecognized type string falls back to string")
func unrecognizedTypeFallsBackToString() {
let prop = MCPPropertySchema(type: "something-unusual", description: nil, enum: nil, items: nil)
let schema = MCPInputSchema(type: "object", properties: ["weird": prop], required: nil)
let parameters = ExternalMCPManager.convertInputSchema(schema)
#expect(parameters.properties["weird"]?.type == "string")
}
@Test("Recognized types (string/number/boolean/array/object) pass through unchanged")
func recognizedTypesPassThrough() {
let types = ["string", "number", "boolean", "array", "object"]
var properties: [String: MCPPropertySchema] = [:]
for t in types {
properties[t] = MCPPropertySchema(type: t, description: nil, enum: nil, items: nil)
}
let schema = MCPInputSchema(type: "object", properties: properties, required: nil)
let parameters = ExternalMCPManager.convertInputSchema(schema)
for t in types {
#expect(parameters.properties[t]?.type == t)
}
}
}