|
28 | 28 | - [Eliciting User Input](#eliciting-user-input) |
29 | 29 | - [Writing MCP Clients](#writing-mcp-clients) |
30 | 30 | - [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream) |
| 31 | + - [Custom Reconnection Scheduling for Non-Persistent Environments](#custom-reconnection-scheduling-for-non-persistent-environments) |
31 | 32 | - [Backwards Compatibility](#backwards-compatibility) |
32 | 33 | - [Documentation](#documentation) |
33 | 34 | - [Contributing](#contributing) |
@@ -1480,6 +1481,175 @@ This setup allows you to: |
1480 | 1481 | - Provide custom documentation URLs |
1481 | 1482 | - Maintain control over the OAuth flow while delegating to an external provider |
1482 | 1483 |
|
| 1484 | +### Custom Reconnection Scheduling for Non-Persistent Environments |
| 1485 | + |
| 1486 | +By default, the Streamable HTTP client transport uses `setTimeout` to schedule automatic reconnections after connection failures. However, this approach doesn't work well in non-persistent environments like serverless functions, mobile apps, or desktop applications that may be suspended. |
| 1487 | + |
| 1488 | +The SDK allows you to provide a custom reconnection scheduler to handle these scenarios: |
| 1489 | + |
| 1490 | +#### Use Cases |
| 1491 | + |
| 1492 | +- **Serverless Functions**: Reconnect immediately on the next function invocation instead of waiting for a timer |
| 1493 | +- **Mobile Apps**: Use platform-specific background scheduling (iOS Background Fetch, Android WorkManager) |
| 1494 | +- **Desktop Apps**: Handle sleep/wake cycles with OS-aware scheduling |
| 1495 | +- **Edge Functions**: Optimize for short-lived execution contexts |
| 1496 | + |
| 1497 | +#### API |
| 1498 | + |
| 1499 | +```typescript |
| 1500 | +type ReconnectionScheduler = ( |
| 1501 | + reconnect: () => void, // Function to call to initiate reconnection |
| 1502 | + delay: number, // Suggested delay in milliseconds |
| 1503 | + attemptCount: number // Current reconnection attempt count (0-indexed) |
| 1504 | +) => void; |
| 1505 | +``` |
| 1506 | + |
| 1507 | +#### Example: Serverless Environment (Immediate Reconnection) |
| 1508 | + |
| 1509 | +```typescript |
| 1510 | +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; |
| 1511 | +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; |
| 1512 | + |
| 1513 | +// Serverless scheduler: reconnect immediately without setTimeout |
| 1514 | +const serverlessScheduler = (reconnect, delay, attemptCount) => { |
| 1515 | + // In serverless, timers don't persist across invocations |
| 1516 | + // Just reconnect immediately - the function will handle retries on next invocation |
| 1517 | + reconnect(); |
| 1518 | +}; |
| 1519 | + |
| 1520 | +const transport = new StreamableHTTPClientTransport( |
| 1521 | + new URL('https://api.example.com/mcp'), |
| 1522 | + { |
| 1523 | + reconnectionOptions: { |
| 1524 | + initialReconnectionDelay: 1000, |
| 1525 | + maxReconnectionDelay: 30000, |
| 1526 | + reconnectionDelayGrowFactor: 2, |
| 1527 | + maxRetries: 5 |
| 1528 | + }, |
| 1529 | + reconnectionScheduler: serverlessScheduler |
| 1530 | + } |
| 1531 | +); |
| 1532 | + |
| 1533 | +const client = new Client({ |
| 1534 | + name: 'serverless-client', |
| 1535 | + version: '1.0.0' |
| 1536 | +}); |
| 1537 | + |
| 1538 | +await client.connect(transport); |
| 1539 | +``` |
| 1540 | + |
| 1541 | +#### Example: Serverless with Deferred Reconnection |
| 1542 | + |
| 1543 | +For true serverless environments where the function may terminate before reconnection, you can store the reconnection callback for the next invocation: |
| 1544 | + |
| 1545 | +```typescript |
| 1546 | +// Global or persistent storage for reconnection callback |
| 1547 | +let storedReconnect: (() => void) | undefined; |
| 1548 | + |
| 1549 | +const deferredScheduler = (reconnect, delay, attemptCount) => { |
| 1550 | + // Store the reconnect callback instead of calling it |
| 1551 | + // In a real app, persist this to a database or queue |
| 1552 | + storedReconnect = reconnect; |
| 1553 | + console.log(`Reconnection scheduled for next invocation (attempt ${attemptCount})`); |
| 1554 | +}; |
| 1555 | + |
| 1556 | +const transport = new StreamableHTTPClientTransport( |
| 1557 | + new URL('https://api.example.com/mcp'), |
| 1558 | + { |
| 1559 | + reconnectionScheduler: deferredScheduler |
| 1560 | + } |
| 1561 | +); |
| 1562 | + |
| 1563 | +// Later, on next function invocation: |
| 1564 | +if (storedReconnect) { |
| 1565 | + console.log('Triggering stored reconnection'); |
| 1566 | + storedReconnect(); |
| 1567 | + storedReconnect = undefined; |
| 1568 | +} |
| 1569 | +``` |
| 1570 | + |
| 1571 | +#### Example: Mobile App with Platform Scheduling |
| 1572 | + |
| 1573 | +```typescript |
| 1574 | +// iOS/Android mobile app using platform-specific background tasks |
| 1575 | +const mobileScheduler = (reconnect, delay, attemptCount) => { |
| 1576 | + if (attemptCount > 3) { |
| 1577 | + console.log('Too many reconnection attempts, giving up'); |
| 1578 | + return; |
| 1579 | + } |
| 1580 | + |
| 1581 | + // Use native background task API (pseudocode) |
| 1582 | + BackgroundTaskManager.schedule({ |
| 1583 | + taskId: 'mcp-reconnect', |
| 1584 | + delay: delay, |
| 1585 | + callback: () => { |
| 1586 | + console.log(`Background reconnection attempt ${attemptCount}`); |
| 1587 | + reconnect(); |
| 1588 | + } |
| 1589 | + }); |
| 1590 | +}; |
| 1591 | + |
| 1592 | +const transport = new StreamableHTTPClientTransport( |
| 1593 | + new URL('https://api.example.com/mcp'), |
| 1594 | + { |
| 1595 | + reconnectionOptions: { |
| 1596 | + initialReconnectionDelay: 5000, |
| 1597 | + maxReconnectionDelay: 60000, |
| 1598 | + reconnectionDelayGrowFactor: 1.5, |
| 1599 | + maxRetries: 3 |
| 1600 | + }, |
| 1601 | + reconnectionScheduler: mobileScheduler |
| 1602 | + } |
| 1603 | +); |
| 1604 | +``` |
| 1605 | + |
| 1606 | +#### Example: Desktop App with Power Management |
| 1607 | + |
| 1608 | +```typescript |
| 1609 | +// Desktop app that respects system sleep/wake cycles |
| 1610 | +const desktopScheduler = (reconnect, delay, attemptCount) => { |
| 1611 | + const timeoutId = setTimeout(() => { |
| 1612 | + // Check if system was sleeping |
| 1613 | + const actualElapsed = Date.now() - scheduleTime; |
| 1614 | + if (actualElapsed > delay * 1.5) { |
| 1615 | + console.log('System was likely sleeping, reconnecting immediately'); |
| 1616 | + reconnect(); |
| 1617 | + } else { |
| 1618 | + reconnect(); |
| 1619 | + } |
| 1620 | + }, delay); |
| 1621 | + |
| 1622 | + const scheduleTime = Date.now(); |
| 1623 | + |
| 1624 | + // Handle system wake events (pseudocode) |
| 1625 | + powerMonitor.on('resume', () => { |
| 1626 | + clearTimeout(timeoutId); |
| 1627 | + console.log('System resumed, reconnecting immediately'); |
| 1628 | + reconnect(); |
| 1629 | + }); |
| 1630 | +}; |
| 1631 | + |
| 1632 | +const transport = new StreamableHTTPClientTransport( |
| 1633 | + new URL('https://api.example.com/mcp'), |
| 1634 | + { |
| 1635 | + reconnectionScheduler: desktopScheduler |
| 1636 | + } |
| 1637 | +); |
| 1638 | +``` |
| 1639 | + |
| 1640 | +#### Default Behavior |
| 1641 | + |
| 1642 | +If no custom scheduler is provided, the transport uses the default `setTimeout`-based scheduler: |
| 1643 | + |
| 1644 | +```typescript |
| 1645 | +// Default scheduler (built-in) |
| 1646 | +const defaultScheduler = (reconnect, delay, attemptCount) => { |
| 1647 | + setTimeout(reconnect, delay); |
| 1648 | +}; |
| 1649 | +``` |
| 1650 | + |
| 1651 | +This default scheduler works well for traditional long-running applications but may not be suitable for environments with lifecycle constraints. |
| 1652 | + |
1483 | 1653 | ### Backwards Compatibility |
1484 | 1654 |
|
1485 | 1655 | Clients and servers with StreamableHttp transport can maintain [backwards compatibility](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility) with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows |
|
0 commit comments