Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions Packages/OsaurusCore/Managers/MCPProviderManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,7 @@ public final class MCPProviderManager: ObservableObject {

/// Test connection to a provider without persisting
public func testConnection(url: String, token: String?, headers: [String: String]) async throws -> Int {
guard let endpoint = URL(string: url) else {
throw MCPProviderError.invalidURL
}
let endpoint = try Self.validatedHTTPSEndpoint(from: url)

// Create temporary transport
let configuration = URLSessionConfiguration.default
Expand Down Expand Up @@ -361,10 +359,22 @@ public final class MCPProviderManager: ObservableObject {

// MARK: - Private Helpers

private func createTransport(for provider: MCPProvider) throws -> HTTPClientTransport {
guard let endpoint = URL(string: provider.url) else {
throw MCPProviderError.invalidURL
nonisolated public static func validatedHTTPSEndpoint(from url: String) throws -> URL {
let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
guard
let endpoint = URL(string: trimmedURL),
let scheme = endpoint.scheme?.lowercased(),
scheme == "http" || scheme == "https",
let host = endpoint.host,
!host.isEmpty
else {
throw MCPProviderError.unsupportedTransport
}
return endpoint
}

private func createTransport(for provider: MCPProvider) throws -> HTTPClientTransport {
let endpoint = try Self.validatedHTTPSEndpoint(from: provider.url)

let urlConfig = URLSessionConfiguration.default

Expand Down Expand Up @@ -454,6 +464,7 @@ public enum MCPProviderError: LocalizedError {
case providerDisabled
case notConnected
case invalidURL
case unsupportedTransport
case timeout
case toolExecutionFailed(String)
case connectionFailed(String)
Expand All @@ -468,6 +479,8 @@ public enum MCPProviderError: LocalizedError {
return "Not connected to provider"
case .invalidURL:
return "Invalid server URL"
case .unsupportedTransport:
return "Remote MCP providers support HTTP/SSE endpoints only. Command-based stdio servers are not supported yet."
case .timeout:
return "Request timed out"
case .toolExecutionFailed(let message):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// MCPProviderEndpointValidationTests.swift
// osaurusTests
//
// Pins the supported transport for remote MCP providers so command-based
// stdio provider strings fail with a clear message instead of looking like
// malformed HTTP endpoints.
//

import Foundation
import Testing

@testable import OsaurusCore

struct MCPProviderEndpointValidationTests {
@Test func httpEndpointIsAccepted() throws {
let url = try MCPProviderManager.validatedHTTPSEndpoint(from: "http://127.0.0.1:3000/mcp")
#expect(url.absoluteString == "http://127.0.0.1:3000/mcp")
}

@Test func httpsSSEEndpointIsAccepted() throws {
let url = try MCPProviderManager.validatedHTTPSEndpoint(from: " https://mcp.example.com/sse ")
#expect(url.absoluteString == "https://mcp.example.com/sse")
}

@Test func stdioCommandIsRejectedWithTransportMessage() throws {
expectUnsupportedTransport(from: "python -m some_mcp.server")
}

@Test func nonHTTPURLIsRejectedWithTransportMessage() throws {
expectUnsupportedTransport(from: "stdio://some_mcp.server")
}

private func expectUnsupportedTransport(from endpoint: String) {
do {
_ = try MCPProviderManager.validatedHTTPSEndpoint(from: endpoint)
Issue.record("Expected unsupported transport for \(endpoint)")
} catch MCPProviderError.unsupportedTransport {
// Expected.
} catch {
Issue.record("Expected unsupported transport, got \(error)")
}
}
}
22 changes: 20 additions & 2 deletions Packages/OsaurusCore/Views/Settings/ProvidersView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,18 @@ private struct ProviderEditSheet: View {
)

MCPStyledTextField(
label: "URL",
placeholder: "https://mcp.example.com",
label: "HTTP/SSE URL",
placeholder: "https://mcp.example.com/sse",
text: $url,
isMonospaced: true
)

if let urlValidationMessage {
Text(verbatim: urlValidationMessage)
.font(.system(size: 11))
.foregroundColor(themeManager.currentTheme.errorColor)
}

MCPStyledSecureField(
label: "Bearer Token",
placeholder: "Optional - stored securely in Keychain",
Expand Down Expand Up @@ -875,6 +881,18 @@ private struct ProviderEditSheet: View {
private var canSave: Bool {
!name.trimmingCharacters(in: .whitespaces).isEmpty
&& !url.trimmingCharacters(in: .whitespaces).isEmpty
&& urlValidationMessage == nil
}

private var urlValidationMessage: String? {
let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedURL.isEmpty else { return nil }
do {
_ = try MCPProviderManager.validatedHTTPSEndpoint(from: trimmedURL)
return nil
} catch {
return error.localizedDescription
}
}

private func loadProvider() {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Osaurus is a full MCP (Model Context Protocol) server. Give any MCP-compatible c
}
```

Also an MCP client -- aggregate tools from remote MCP servers into Osaurus. See the [Remote MCP Providers Guide](docs/REMOTE_MCP_PROVIDERS.md) for details.
Also an HTTP/SSE MCP client -- aggregate tools from remote MCP servers into Osaurus. Command-based stdio providers are not supported by that remote-provider path yet. See the [Remote MCP Providers Guide](docs/REMOTE_MCP_PROVIDERS.md) for details.

## Tools & Plugins

Expand Down
3 changes: 2 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ See [INFERENCE_RUNTIME.md](./INFERENCE_RUNTIME.md) for the full runtime architec

### Remote MCP Providers

**Purpose:** Connect to external MCP servers and aggregate their tools.
**Purpose:** Connect to external MCP servers over HTTP/SSE and aggregate their tools.

**Components:**

Expand All @@ -220,6 +220,7 @@ See [INFERENCE_RUNTIME.md](./INFERENCE_RUNTIME.md) for the full runtime architec
- Configurable discovery and execution timeouts
- Tool namespacing (prefixed with provider name)
- Streaming support (optional)
- Command-based stdio providers are not supported by this remote-provider path yet

---

Expand Down
14 changes: 11 additions & 3 deletions docs/REMOTE_MCP_PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

Remote MCP Providers allow you to connect Osaurus to external MCP (Model Context Protocol) servers, aggregating their tools into your Osaurus instance.

Remote MCP Providers currently support HTTP/SSE endpoints only. Command-based stdio MCP servers, such as `python -m some_mcp.server`, are not supported as remote providers yet.

---

## Overview

With Remote MCP Providers, you can:

- Connect to any MCP server over HTTP/SSE
- Connect to MCP servers exposed over HTTP/SSE
- Automatically discover and register remote tools
- Use remote tools alongside local plugins
- Aggregate tools from multiple MCP servers
Expand All @@ -25,7 +27,7 @@ This is different from Remote Providers (which provide inference endpoints). Rem
2. Click **Providers** in the sidebar
3. Scroll to the **MCP Providers** section
4. Click **Add MCP Provider**
5. Enter the MCP server URL
5. Enter the MCP server HTTP/SSE URL
6. Configure authentication if required
7. Click **Save**

Expand All @@ -38,7 +40,7 @@ This is different from Remote Providers (which provide inference endpoints). Rem
| Setting | Description |
| ----------- | ----------------------------------- |
| **Name** | Display name for the provider |
| **URL** | Full URL to the MCP server endpoint |
| **URL** | Full HTTP/SSE URL to the MCP server endpoint |
| **Enabled** | Whether the provider is active |

### Authentication
Expand Down Expand Up @@ -165,6 +167,12 @@ Before saving a provider, you can test the connection:
- Check the URL is correct (including protocol and port)
- Ensure no firewall is blocking the connection

**"Remote MCP providers support HTTP/SSE endpoints only"**

- Use an `http://` or `https://` MCP endpoint, such as `https://mcp.example.com/sse`
- Do not paste a local stdio command into the URL field
- To expose Osaurus itself to a stdio MCP client, use `osaurus mcp` from that client configuration; this is separate from connecting Osaurus to remote providers

**"Authentication failed"**

- Verify your token is correct
Expand Down
Loading