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.
66 lines
2.7 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|