Skip to content
Open
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: 17 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:

jobs:
test:
timeout-minutes: 5
timeout-minutes: 10
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
Expand Down Expand Up @@ -61,23 +61,19 @@ jobs:
with:
swift-version: 6.1.0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Build Swift executables
run: |
swift build --product mcp-everything-client
swift build --product mcp-everything-server

- name: Run client conformance tests
uses: modelcontextprotocol/conformance@v0.1.11
uses: modelcontextprotocol/conformance@v0.1.15
with:
mode: client
command: '.build/debug/mcp-everything-client'
suite: 'core'
expected-failures: './conformance-baseline.yml'
node-version: '22'

- name: Start server for testing
run: |
Expand All @@ -86,12 +82,13 @@ jobs:
sleep 3

- name: Run server conformance tests
uses: modelcontextprotocol/conformance@v0.1.11
uses: modelcontextprotocol/conformance@v0.1.15
with:
mode: server
url: 'http://localhost:3001/mcp'
suite: 'core'
expected-failures: './conformance-baseline.yml'
node-version: '22'

- name: Cleanup server
if: always()
Expand All @@ -100,6 +97,18 @@ jobs:
kill $SERVER_PID 2>/dev/null || true
fi

documentation:
name: Documentation
runs-on: macos-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.1.0"
- name: Build Documentation
run: swift package generate-documentation --target MCP --warnings-as-errors

static-linux-sdk-build:
name: Linux Static SDK Build (${{ matrix.swift-version }} - ${{ matrix.os }})
strategy:
Expand Down
4 changes: 4 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [MCP]
20 changes: 19 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let package = Package(
targets: ["MCPConformanceClient"])
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-docc-plugin", branch: "main"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://github.com/mattt/eventsource.git", from: "1.1.0"),
Expand Down
198 changes: 198 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ of the MCP specification.
- [Initialize Hook](#initialize-hook)
- [Graceful Shutdown](#graceful-shutdown)
- [Transports](#transports)
- [Authentication](#authentication)
- [Client: Client Credentials Flow](#client-client-credentials-flow)
- [Client: Authorization Code Flow](#client-authorization-code-flow)
- [Client: Custom Token Provider](#client-custom-token-provider)
- [Client: Custom Token Storage](#client-custom-token-storage)
- [Client: private\_key\_jwt Authentication](#client-private_key_jwt-authentication)
- [Client: Endpoint Overrides](#client-endpoint-overrides)
- [Server: Serving Protected Resource Metadata](#server-serving-protected-resource-metadata)
- [Server: Validating Bearer Tokens](#server-validating-bearer-tokens)
- [Platform Availability](#platform-availability)
- [Debugging and Logging](#debugging-and-logging)
- [Additional Resources](#additional-resources)
Expand Down Expand Up @@ -1341,6 +1350,195 @@ public actor MyCustomTransport: Transport {
}
```

## Authentication

`HTTPClientTransport` supports OAuth 2.1 Bearer token authorization per the
[MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
When a server returns `401 Unauthorized` or `403 Forbidden`, the transport automatically:

1. Discovers Protected Resource Metadata (RFC 9728) at `/.well-known/oauth-protected-resource`
2. Discovers Authorization Server Metadata (RFC 8414 / OIDC Discovery 1.0)
3. Registers the client dynamically (RFC 7591) if needed
4. Acquires a Bearer token using the configured grant flow (PKCE enforced)
5. Retries the original request with the token attached

Authorization is opt-in and disabled by default.
Pass an `OAuthAuthorizer` to `HTTPClientTransport(authorizer:)` to enable it.

### Client: Client Credentials Flow

Machine-to-machine authentication using a pre-shared client secret:

```swift
let config = OAuthConfiguration(
grantType: .clientCredentials,
authentication: .clientSecretBasic(clientID: "my-app", clientSecret: "s3cr3t")
)
let authorizer = OAuthAuthorizer(configuration: config)
let transport = HTTPClientTransport(
endpoint: URL(string: "https://api.example.com/mcp")!,
authorizer: authorizer
)
let client = Client(name: "MyClient", version: "1.0.0")
try await client.connect(transport: transport)
```

### Client: Authorization Code Flow

Interactive, browser-based authentication with PKCE.
Implement `OAuthAuthorizationDelegate` to open the authorization URL and capture the redirect:

```swift
struct MyAuthDelegate: OAuthAuthorizationDelegate {
func presentAuthorizationURL(_ url: URL) async throws -> URL {
// Open the URL in a browser/webview and wait for the callback redirect URI.
// The returned URL must include the authorization code and state parameters.
return try await openBrowserAndWaitForCallback(url)
}
}

let config = OAuthConfiguration(
grantType: .authorizationCode,
authentication: .none(clientID: "my-app"),
authorizationDelegate: MyAuthDelegate()
)
let authorizer = OAuthAuthorizer(configuration: config)
let transport = HTTPClientTransport(
endpoint: URL(string: "https://api.example.com/mcp")!,
authorizer: authorizer
)
```

### Client: Custom Token Provider

Supply an externally acquired token (e.g., from a system credential store) via `accessTokenProvider`.
The SDK calls this closure after discovery completes. Return `nil` to fall back to the configured grant flow:

```swift
let config = OAuthConfiguration(
grantType: .clientCredentials,
authentication: .none(clientID: "my-app"),
accessTokenProvider: { context, session in
// context contains the discovered resource URI, token endpoint, scopes, etc.
return try await KeychainTokenStore.shared.loadToken(for: context.resource)
}
)
```

### Client: Custom Token Storage

By default, tokens are stored in memory and lost when the process exits.
To persist tokens across sessions, implement `TokenStorage` and pass it to `OAuthAuthorizer`:

```swift
final class KeychainTokenStorage: TokenStorage {
func save(_ token: OAuthAccessToken) {
// Encode and store token.value in the system Keychain
}

func load() -> OAuthAccessToken? {
// Load and decode token from the Keychain
return nil
}

func clear() {
// Delete from the Keychain
}
}

let authorizer = OAuthAuthorizer(
configuration: config,
tokenStorage: KeychainTokenStorage()
)
```

### Client: `private_key_jwt` Authentication

Authenticate to the token endpoint using an asymmetric key (RFC 7523).
The SDK provides a built-in ES256 helper for P-256 keys:

```swift
let config = OAuthConfiguration(
grantType: .clientCredentials,
authentication: .privateKeyJWT(
clientID: "my-app",
assertionFactory: { tokenEndpoint, clientID in
try OAuthConfiguration.makePrivateKeyJWTAssertion(
clientID: clientID,
tokenEndpoint: tokenEndpoint,
privateKeyPEM: myEC256PrivateKeyPEM // PEM-encoded P-256 private key
)
}
)
)
```

### Client: Endpoint Overrides

Skip automatic discovery by providing explicit endpoint URLs.
Useful when the server does not publish well-known metadata documents:

```swift
let config = OAuthConfiguration(
grantType: .clientCredentials,
authentication: .clientSecretBasic(clientID: "app", clientSecret: "secret"),
endpointOverrides: OAuthConfiguration.EndpointOverrides(
tokenEndpoint: URL(string: "https://auth.example.com/oauth/token")!
)
)
```

### Server: Serving Protected Resource Metadata

Per the MCP authorization specification, servers **MUST** serve Protected Resource Metadata
at `/.well-known/oauth-protected-resource` so clients can discover authorization server endpoints.

Use `ProtectedResourceMetadataValidator` as the first validator in your pipeline so that
unauthenticated discovery requests are handled before the bearer token check:

```swift
let metadata = OAuthProtectedResourceServerMetadata(
resource: "https://api.example.com",
authorizationServers: [URL(string: "https://auth.example.com")!],
scopesSupported: ["read", "write"]
)
let metadataValidator = ProtectedResourceMetadataValidator(metadata: metadata)
```

### Server: Validating Bearer Tokens

Use `BearerTokenValidator` to authenticate incoming requests.
Your `tokenValidator` closure **MUST** verify the token's `aud` claim to prevent
token substitution attacks where a token intended for another resource is replayed against your server:

```swift
let resourceIdentifier = URL(string: "https://api.example.com")!

let bearerValidator = BearerTokenValidator(
resourceMetadataURL: URL(string: "https://api.example.com/.well-known/oauth-protected-resource")!,
resourceIdentifier: resourceIdentifier,
tokenValidator: { token, request, context in
guard let claims = try? verifyAndDecodeJWT(token) else {
return .invalidToken(errorDescription: "Token verification failed")
}
// Pass audience and expiry to BearerTokenInfo; the SDK validates the
// audience claim against resourceIdentifier automatically.
return .valid(BearerTokenInfo(
audience: claims.audience,
expiresAt: claims.expiresAt
))
}
)

let pipeline = StandardValidationPipeline(validators: [
metadataValidator, // serves /.well-known/oauth-protected-resource unauthenticated
bearerValidator, // validates Bearer tokens on all other requests
AcceptHeaderValidator(mode: .sseRequired),
ContentTypeValidator(),
SessionValidator(),
])
```

## Platform Availability

The Swift SDK has the following platform requirements:
Expand Down
Loading