Skip to content

Commit 6132fd4

Browse files
movetzpaaloeyewtiyehorsobko-mac
authored
V0.12.x (#215)
* fix #203 NetworkTransport race, with main-isolated variables (#206) Co-authored-by: wti <wti@users.noreply.github.com> * feat/auth: Authentication implementation (#205) * feat: added authentication * feat(auth): improvement to error handling * feat(auth): updated errors and tests in OAuthClientRegistrar * feat(auth): updated ci conformance package version * feat(auth): bumped node version * feat(auth): updated ci conformance step * feat(auth): improvement to Linux support * chore: added docc plugin * chore: added spi yml * chore: added documentation step to ci * chore: increased timeout for test from 5 to 10m --------- Co-authored-by: Paal Øye-Strømme <paal.o.eye@gmail.com> Co-authored-by: wti <wti@users.noreply.github.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
1 parent 973b46b commit 6132fd4

39 files changed

+10925
-199
lines changed

.github/workflows/ci.yml

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ permissions:
1212

1313
jobs:
1414
test:
15-
timeout-minutes: 5
15+
timeout-minutes: 10
1616
strategy:
1717
matrix:
1818
os: [macos-latest, ubuntu-latest]
@@ -61,23 +61,19 @@ jobs:
6161
with:
6262
swift-version: 6.1.0
6363

64-
- name: Setup Node.js
65-
uses: actions/setup-node@v4
66-
with:
67-
node-version: '20'
68-
6964
- name: Build Swift executables
7065
run: |
7166
swift build --product mcp-everything-client
7267
swift build --product mcp-everything-server
7368
7469
- name: Run client conformance tests
75-
uses: modelcontextprotocol/conformance@v0.1.11
70+
uses: modelcontextprotocol/conformance@v0.1.15
7671
with:
7772
mode: client
7873
command: '.build/debug/mcp-everything-client'
7974
suite: 'core'
8075
expected-failures: './conformance-baseline.yml'
76+
node-version: '22'
8177

8278
- name: Start server for testing
8379
run: |
@@ -86,12 +82,13 @@ jobs:
8682
sleep 3
8783
8884
- name: Run server conformance tests
89-
uses: modelcontextprotocol/conformance@v0.1.11
85+
uses: modelcontextprotocol/conformance@v0.1.15
9086
with:
9187
mode: server
9288
url: 'http://localhost:3001/mcp'
9389
suite: 'core'
9490
expected-failures: './conformance-baseline.yml'
91+
node-version: '22'
9592

9693
- name: Cleanup server
9794
if: always()
@@ -100,6 +97,18 @@ jobs:
10097
kill $SERVER_PID 2>/dev/null || true
10198
fi
10299
100+
documentation:
101+
name: Documentation
102+
runs-on: macos-latest
103+
timeout-minutes: 10
104+
steps:
105+
- uses: actions/checkout@v4
106+
- uses: swift-actions/setup-swift@v2
107+
with:
108+
swift-version: "6.1.0"
109+
- name: Build Documentation
110+
run: swift package generate-documentation --target MCP --warnings-as-errors
111+
103112
static-linux-sdk-build:
104113
name: Linux Static SDK Build (${{ matrix.swift-version }} - ${{ matrix.os }})
105114
strategy:

.spi.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: 1
2+
builder:
3+
configs:
4+
- documentation_targets: [MCP]

Package.resolved

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let package = Package(
2626
targets: ["MCPConformanceClient"])
2727
],
2828
dependencies: [
29+
.package(url: "https://github.com/swiftlang/swift-docc-plugin", branch: "main"),
2930
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
3031
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
3132
.package(url: "https://github.com/mattt/eventsource.git", from: "1.1.0"),

README.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ of the MCP specification.
4343
- [Initialize Hook](#initialize-hook)
4444
- [Graceful Shutdown](#graceful-shutdown)
4545
- [Transports](#transports)
46+
- [Authentication](#authentication)
47+
- [Client: Client Credentials Flow](#client-client-credentials-flow)
48+
- [Client: Authorization Code Flow](#client-authorization-code-flow)
49+
- [Client: Custom Token Provider](#client-custom-token-provider)
50+
- [Client: Custom Token Storage](#client-custom-token-storage)
51+
- [Client: private\_key\_jwt Authentication](#client-private_key_jwt-authentication)
52+
- [Client: Endpoint Overrides](#client-endpoint-overrides)
53+
- [Server: Serving Protected Resource Metadata](#server-serving-protected-resource-metadata)
54+
- [Server: Validating Bearer Tokens](#server-validating-bearer-tokens)
4655
- [Platform Availability](#platform-availability)
4756
- [Debugging and Logging](#debugging-and-logging)
4857
- [Additional Resources](#additional-resources)
@@ -1341,6 +1350,195 @@ public actor MyCustomTransport: Transport {
13411350
}
13421351
```
13431352

1353+
## Authentication
1354+
1355+
`HTTPClientTransport` supports OAuth 2.1 Bearer token authorization per the
1356+
[MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
1357+
When a server returns `401 Unauthorized` or `403 Forbidden`, the transport automatically:
1358+
1359+
1. Discovers Protected Resource Metadata (RFC 9728) at `/.well-known/oauth-protected-resource`
1360+
2. Discovers Authorization Server Metadata (RFC 8414 / OIDC Discovery 1.0)
1361+
3. Registers the client dynamically (RFC 7591) if needed
1362+
4. Acquires a Bearer token using the configured grant flow (PKCE enforced)
1363+
5. Retries the original request with the token attached
1364+
1365+
Authorization is opt-in and disabled by default.
1366+
Pass an `OAuthAuthorizer` to `HTTPClientTransport(authorizer:)` to enable it.
1367+
1368+
### Client: Client Credentials Flow
1369+
1370+
Machine-to-machine authentication using a pre-shared client secret:
1371+
1372+
```swift
1373+
let config = OAuthConfiguration(
1374+
grantType: .clientCredentials,
1375+
authentication: .clientSecretBasic(clientID: "my-app", clientSecret: "s3cr3t")
1376+
)
1377+
let authorizer = OAuthAuthorizer(configuration: config)
1378+
let transport = HTTPClientTransport(
1379+
endpoint: URL(string: "https://api.example.com/mcp")!,
1380+
authorizer: authorizer
1381+
)
1382+
let client = Client(name: "MyClient", version: "1.0.0")
1383+
try await client.connect(transport: transport)
1384+
```
1385+
1386+
### Client: Authorization Code Flow
1387+
1388+
Interactive, browser-based authentication with PKCE.
1389+
Implement `OAuthAuthorizationDelegate` to open the authorization URL and capture the redirect:
1390+
1391+
```swift
1392+
struct MyAuthDelegate: OAuthAuthorizationDelegate {
1393+
func presentAuthorizationURL(_ url: URL) async throws -> URL {
1394+
// Open the URL in a browser/webview and wait for the callback redirect URI.
1395+
// The returned URL must include the authorization code and state parameters.
1396+
return try await openBrowserAndWaitForCallback(url)
1397+
}
1398+
}
1399+
1400+
let config = OAuthConfiguration(
1401+
grantType: .authorizationCode,
1402+
authentication: .none(clientID: "my-app"),
1403+
authorizationDelegate: MyAuthDelegate()
1404+
)
1405+
let authorizer = OAuthAuthorizer(configuration: config)
1406+
let transport = HTTPClientTransport(
1407+
endpoint: URL(string: "https://api.example.com/mcp")!,
1408+
authorizer: authorizer
1409+
)
1410+
```
1411+
1412+
### Client: Custom Token Provider
1413+
1414+
Supply an externally acquired token (e.g., from a system credential store) via `accessTokenProvider`.
1415+
The SDK calls this closure after discovery completes. Return `nil` to fall back to the configured grant flow:
1416+
1417+
```swift
1418+
let config = OAuthConfiguration(
1419+
grantType: .clientCredentials,
1420+
authentication: .none(clientID: "my-app"),
1421+
accessTokenProvider: { context, session in
1422+
// context contains the discovered resource URI, token endpoint, scopes, etc.
1423+
return try await KeychainTokenStore.shared.loadToken(for: context.resource)
1424+
}
1425+
)
1426+
```
1427+
1428+
### Client: Custom Token Storage
1429+
1430+
By default, tokens are stored in memory and lost when the process exits.
1431+
To persist tokens across sessions, implement `TokenStorage` and pass it to `OAuthAuthorizer`:
1432+
1433+
```swift
1434+
final class KeychainTokenStorage: TokenStorage {
1435+
func save(_ token: OAuthAccessToken) {
1436+
// Encode and store token.value in the system Keychain
1437+
}
1438+
1439+
func load() -> OAuthAccessToken? {
1440+
// Load and decode token from the Keychain
1441+
return nil
1442+
}
1443+
1444+
func clear() {
1445+
// Delete from the Keychain
1446+
}
1447+
}
1448+
1449+
let authorizer = OAuthAuthorizer(
1450+
configuration: config,
1451+
tokenStorage: KeychainTokenStorage()
1452+
)
1453+
```
1454+
1455+
### Client: `private_key_jwt` Authentication
1456+
1457+
Authenticate to the token endpoint using an asymmetric key (RFC 7523).
1458+
The SDK provides a built-in ES256 helper for P-256 keys:
1459+
1460+
```swift
1461+
let config = OAuthConfiguration(
1462+
grantType: .clientCredentials,
1463+
authentication: .privateKeyJWT(
1464+
clientID: "my-app",
1465+
assertionFactory: { tokenEndpoint, clientID in
1466+
try OAuthConfiguration.makePrivateKeyJWTAssertion(
1467+
clientID: clientID,
1468+
tokenEndpoint: tokenEndpoint,
1469+
privateKeyPEM: myEC256PrivateKeyPEM // PEM-encoded P-256 private key
1470+
)
1471+
}
1472+
)
1473+
)
1474+
```
1475+
1476+
### Client: Endpoint Overrides
1477+
1478+
Skip automatic discovery by providing explicit endpoint URLs.
1479+
Useful when the server does not publish well-known metadata documents:
1480+
1481+
```swift
1482+
let config = OAuthConfiguration(
1483+
grantType: .clientCredentials,
1484+
authentication: .clientSecretBasic(clientID: "app", clientSecret: "secret"),
1485+
endpointOverrides: OAuthConfiguration.EndpointOverrides(
1486+
tokenEndpoint: URL(string: "https://auth.example.com/oauth/token")!
1487+
)
1488+
)
1489+
```
1490+
1491+
### Server: Serving Protected Resource Metadata
1492+
1493+
Per the MCP authorization specification, servers **MUST** serve Protected Resource Metadata
1494+
at `/.well-known/oauth-protected-resource` so clients can discover authorization server endpoints.
1495+
1496+
Use `ProtectedResourceMetadataValidator` as the first validator in your pipeline so that
1497+
unauthenticated discovery requests are handled before the bearer token check:
1498+
1499+
```swift
1500+
let metadata = OAuthProtectedResourceServerMetadata(
1501+
resource: "https://api.example.com",
1502+
authorizationServers: [URL(string: "https://auth.example.com")!],
1503+
scopesSupported: ["read", "write"]
1504+
)
1505+
let metadataValidator = ProtectedResourceMetadataValidator(metadata: metadata)
1506+
```
1507+
1508+
### Server: Validating Bearer Tokens
1509+
1510+
Use `BearerTokenValidator` to authenticate incoming requests.
1511+
Your `tokenValidator` closure **MUST** verify the token's `aud` claim to prevent
1512+
token substitution attacks where a token intended for another resource is replayed against your server:
1513+
1514+
```swift
1515+
let resourceIdentifier = URL(string: "https://api.example.com")!
1516+
1517+
let bearerValidator = BearerTokenValidator(
1518+
resourceMetadataURL: URL(string: "https://api.example.com/.well-known/oauth-protected-resource")!,
1519+
resourceIdentifier: resourceIdentifier,
1520+
tokenValidator: { token, request, context in
1521+
guard let claims = try? verifyAndDecodeJWT(token) else {
1522+
return .invalidToken(errorDescription: "Token verification failed")
1523+
}
1524+
// Pass audience and expiry to BearerTokenInfo; the SDK validates the
1525+
// audience claim against resourceIdentifier automatically.
1526+
return .valid(BearerTokenInfo(
1527+
audience: claims.audience,
1528+
expiresAt: claims.expiresAt
1529+
))
1530+
}
1531+
)
1532+
1533+
let pipeline = StandardValidationPipeline(validators: [
1534+
metadataValidator, // serves /.well-known/oauth-protected-resource unauthenticated
1535+
bearerValidator, // validates Bearer tokens on all other requests
1536+
AcceptHeaderValidator(mode: .sseRequired),
1537+
ContentTypeValidator(),
1538+
SessionValidator(),
1539+
])
1540+
```
1541+
13441542
## Platform Availability
13451543

13461544
The Swift SDK has the following platform requirements:

0 commit comments

Comments
 (0)