Skip to content

Commit ebb2773

Browse files
committed
feat: automatically detect transport
1 parent 4a49891 commit ebb2773

File tree

4 files changed

+450
-27
lines changed

4 files changed

+450
-27
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,50 @@ npx install-mcp https://api.example.com/mcp --client claude \
5353
--header "X-API-Key: secret-key"
5454
```
5555

56+
### Transport Methods for Remote Servers
57+
58+
When installing remote servers (URLs), the CLI needs to know which transport method the server uses. There are two transport methods:
59+
60+
- **Streamable HTTP** (modern, recommended)
61+
- **SSE** (legacy)
62+
63+
The CLI handles this in several ways:
64+
65+
#### Automatic Detection
66+
67+
By default, the CLI will automatically detect the transport method:
68+
69+
```bash
70+
npx install-mcp https://api.example.com/mcp --client claude
71+
# Output: Detecting transport type... this may take a few seconds.
72+
# Output: We've detected that this server uses the streamable HTTP transport method. Is this correct? (Y/n)
73+
```
74+
75+
If the detection succeeds, it will ask you to confirm. If you answer "no", it will use the other transport method.
76+
77+
#### Manual Specification
78+
79+
You can skip detection by specifying the transport method directly:
80+
81+
```bash
82+
# For streamable HTTP servers
83+
npx install-mcp https://api.example.com/mcp --client claude --transport http
84+
85+
# For legacy SSE servers
86+
npx install-mcp https://api.example.com/mcp --client claude --transport sse
87+
```
88+
89+
#### Fallback to Manual Questions
90+
91+
If auto-detection fails, the CLI will ask you directly:
92+
93+
```
94+
Could not auto-detect transport type, please answer the following questions:
95+
Does this server support the streamable HTTP transport method? (Y/n)
96+
```
97+
98+
Note: This only applies to URL-based installations. Package names and custom commands don't require transport selection.
99+
56100
where `<client>` is one of the following:
57101

58102
- `claude`

src/commands/install.test.ts

Lines changed: 157 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import type { ArgumentsCamelCase } from 'yargs'
33
import { handler, InstallArgv } from './install'
44
import * as clientConfig from '../client-config'
55
import { logger } from '../logger'
6+
import * as detectTransport from '../detect-transport'
67

78
// Mock dependencies
89
jest.mock('../client-config')
910
jest.mock('../logger')
11+
jest.mock('../detect-transport')
1012

1113
const mockClientConfig = clientConfig as jest.Mocked<typeof clientConfig>
1214
const mockLogger = logger as jest.Mocked<typeof logger>
15+
const mockDetectTransport = detectTransport as jest.Mocked<typeof detectTransport>
1316

1417
describe('install command', () => {
1518
beforeEach(() => {
@@ -38,6 +41,7 @@ describe('install command', () => {
3841
configKey: 'mcpServers',
3942
})
4043
mockLogger.prompt.mockResolvedValue('test-package')
44+
mockDetectTransport.detectMcpTransport.mockResolvedValue('unknown')
4145
})
4246

4347
afterEach(() => {
@@ -106,11 +110,21 @@ describe('install command', () => {
106110
$0: 'install-mcp',
107111
}
108112

109-
// Mock the transport confirmation prompts (defaults to http)
110-
mockLogger.prompt.mockResolvedValueOnce(true) // supports streamable HTTP
113+
// Mock auto-detection returns http and user confirms
114+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
115+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
111116

112117
await handler(argv)
113118

119+
expect(mockDetectTransport.detectMcpTransport).toHaveBeenCalledWith('https://example.com/server', {
120+
timeoutMs: 5000,
121+
headers: undefined,
122+
})
123+
expect(mockLogger.info).toHaveBeenCalledWith('Detecting transport type... this may take a few seconds.')
124+
expect(mockLogger.prompt).toHaveBeenCalledWith(
125+
"We've detected that this server uses the streamable HTTP transport method. Is this correct?",
126+
{ type: 'confirm' },
127+
)
114128
expect(mockClientConfig.writeConfig).toHaveBeenCalledWith(
115129
expect.objectContaining({
116130
mcpServers: {
@@ -186,14 +200,16 @@ describe('install command', () => {
186200
$0: 'install-mcp',
187201
}
188202

189-
// Mock the transport confirmation prompts
190-
mockLogger.prompt.mockResolvedValueOnce(true) // supports streamable HTTP
203+
// Mock auto-detection returns http and user confirms
204+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
205+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
191206

192207
await handler(argv)
193208

194-
expect(mockLogger.prompt).toHaveBeenCalledWith('Does this server support the streamable HTTP transport method?', {
195-
type: 'confirm',
196-
})
209+
expect(mockLogger.prompt).toHaveBeenCalledWith(
210+
"We've detected that this server uses the streamable HTTP transport method. Is this correct?",
211+
{ type: 'confirm' },
212+
)
197213
})
198214

199215
it('should fall back to SSE when HTTP is not supported', async () => {
@@ -205,7 +221,8 @@ describe('install command', () => {
205221
$0: 'install-mcp',
206222
}
207223

208-
// Mock the transport confirmation prompts
224+
// Mock auto-detection returns unknown, then manual prompts
225+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('unknown')
209226
mockLogger.prompt
210227
.mockResolvedValueOnce(false) // doesn't support streamable HTTP
211228
.mockResolvedValueOnce(true) // uses legacy SSE
@@ -239,15 +256,16 @@ describe('install command', () => {
239256
$0: 'install-mcp',
240257
}
241258

242-
// Mock the transport confirmation prompts
259+
// Mock auto-detection returns unknown, then manual prompts fail
260+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('unknown')
243261
mockLogger.prompt
244262
.mockResolvedValueOnce(false) // doesn't support streamable HTTP
245263
.mockResolvedValueOnce(false) // doesn't use legacy SSE
246264

247265
await handler(argv)
248266

249267
expect(mockLogger.error).toHaveBeenCalledWith(
250-
'Server must support either streamable HTTP or legacy SSE transport method.',
268+
'Remote servers must support either streamable HTTP or legacy SSE transport method.',
251269
)
252270
expect(mockClientConfig.writeConfig).not.toHaveBeenCalled()
253271
})
@@ -276,6 +294,10 @@ describe('install command', () => {
276294
$0: 'install-mcp',
277295
}
278296

297+
// Mock auto-detection returns http and user confirms
298+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
299+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
300+
279301
await handler(argv)
280302

281303
expect(mockClientConfig.writeConfig).toHaveBeenCalledWith(
@@ -409,8 +431,9 @@ describe('install command', () => {
409431
$0: 'install-mcp',
410432
}
411433

412-
// Mock the transport confirmation prompts (defaults to http)
413-
mockLogger.prompt.mockResolvedValueOnce(true) // supports streamable HTTP
434+
// Mock auto-detection returns http and user confirms
435+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
436+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
414437

415438
await handler(argv)
416439

@@ -580,6 +603,128 @@ describe('install command', () => {
580603
undefined,
581604
)
582605
})
606+
607+
it('should auto-detect SSE transport and install with confirmation', async () => {
608+
const argv: ArgumentsCamelCase<InstallArgv> = {
609+
client: 'cline',
610+
target: 'https://example.com/server',
611+
yes: true,
612+
_: [],
613+
$0: 'install-mcp',
614+
}
615+
616+
// Mock auto-detection returns sse and user confirms
617+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('sse')
618+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
619+
620+
await handler(argv)
621+
622+
expect(mockLogger.prompt).toHaveBeenCalledWith(
623+
"We've detected that this server uses the SSE transport method. Is this correct?",
624+
{ type: 'confirm' },
625+
)
626+
expect(mockClientConfig.writeConfig).toHaveBeenCalledWith(
627+
expect.objectContaining({
628+
mcpServers: {
629+
'example-com': {
630+
command: 'npx',
631+
args: ['-y', 'supergateway', '--sse', 'https://example.com/server'],
632+
},
633+
},
634+
}),
635+
'cline',
636+
undefined,
637+
)
638+
})
639+
640+
it('should use opposite transport when user rejects auto-detection', async () => {
641+
const argv: ArgumentsCamelCase<InstallArgv> = {
642+
client: 'cline',
643+
target: 'https://example.com/server',
644+
yes: true,
645+
_: [],
646+
$0: 'install-mcp',
647+
}
648+
649+
// Mock auto-detection returns http but user rejects it
650+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
651+
mockLogger.prompt.mockResolvedValueOnce(false) // rejects detected transport
652+
653+
await handler(argv)
654+
655+
expect(mockLogger.info).toHaveBeenCalledWith('Installing as SSE transport method.')
656+
expect(mockClientConfig.writeConfig).toHaveBeenCalledWith(
657+
expect.objectContaining({
658+
mcpServers: {
659+
'example-com': {
660+
command: 'npx',
661+
args: ['-y', 'supergateway', '--sse', 'https://example.com/server'],
662+
},
663+
},
664+
}),
665+
'cline',
666+
undefined,
667+
)
668+
})
669+
670+
it('should fall back to manual questions when auto-detection returns unknown', async () => {
671+
const argv: ArgumentsCamelCase<InstallArgv> = {
672+
client: 'cline',
673+
target: 'https://example.com/server',
674+
yes: true,
675+
_: [],
676+
$0: 'install-mcp',
677+
}
678+
679+
// Mock auto-detection returns unknown
680+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('unknown')
681+
mockLogger.prompt.mockResolvedValueOnce(true) // supports streamable HTTP
682+
683+
await handler(argv)
684+
685+
expect(mockLogger.info).toHaveBeenCalledWith(
686+
'Could not auto-detect transport type, please answer the following questions:',
687+
)
688+
expect(mockLogger.prompt).toHaveBeenCalledWith('Does this server support the streamable HTTP transport method?', {
689+
type: 'confirm',
690+
})
691+
expect(mockClientConfig.writeConfig).toHaveBeenCalledWith(
692+
expect.objectContaining({
693+
mcpServers: {
694+
'example-com': {
695+
command: 'npx',
696+
args: ['-y', 'supergateway', '--streamableHttp', 'https://example.com/server'],
697+
},
698+
},
699+
}),
700+
'cline',
701+
undefined,
702+
)
703+
})
704+
705+
it('should pass headers to detectMcpTransport when provided', async () => {
706+
const argv: ArgumentsCamelCase<InstallArgv> = {
707+
client: 'cline',
708+
target: 'https://example.com/server',
709+
header: ['Authorization: Bearer token123'],
710+
yes: true,
711+
_: [],
712+
$0: 'install-mcp',
713+
}
714+
715+
// Mock auto-detection returns http and user confirms
716+
mockDetectTransport.detectMcpTransport.mockResolvedValueOnce('http')
717+
mockLogger.prompt.mockResolvedValueOnce(true) // confirms detected transport
718+
719+
await handler(argv)
720+
721+
expect(mockDetectTransport.detectMcpTransport).toHaveBeenCalledWith('https://example.com/server', {
722+
timeoutMs: 5000,
723+
headers: {
724+
Authorization: 'Bearer token123',
725+
},
726+
})
727+
})
583728
})
584729

585730
describe('name inference', () => {

src/commands/install.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
setNestedValue,
1111
type ClientConfig,
1212
} from '../client-config'
13+
import { detectMcpTransport } from '../detect-transport'
1314

1415
// Helper to set a server config in a nested structure
1516
function setServerConfig(
@@ -161,25 +162,54 @@ export async function handler(argv: ArgumentsCamelCase<InstallArgv>) {
161162
// Prompt for transport if target is a URL and transport not specified
162163
let transport = argv.transport
163164
if (isUrl(target) && !transport) {
164-
// Ask about streamable HTTP first (default yes)
165-
const supportsStreamableHttp = await logger.prompt(
166-
'Does this server support the streamable HTTP transport method?',
167-
{ type: 'confirm' },
168-
)
165+
// Try to auto-detect the transport type
166+
logger.info('Detecting transport type... this may take a few seconds.')
167+
168+
const detectedTransport = await detectMcpTransport(target, {
169+
timeoutMs: 5000,
170+
headers: argv.header ? parseHeaders(argv.header) : undefined,
171+
})
169172

170-
if (supportsStreamableHttp) {
171-
transport = 'http'
173+
if (detectedTransport === 'http' || detectedTransport === 'sse') {
174+
// We detected a transport type, ask for confirmation
175+
const transportDisplay = detectedTransport === 'http' ? 'streamable HTTP' : 'SSE'
176+
const confirmed = await logger.prompt(
177+
`We've detected that this server uses the ${transportDisplay} transport method. Is this correct?`,
178+
{ type: 'confirm' },
179+
)
180+
181+
if (confirmed) {
182+
transport = detectedTransport
183+
} else {
184+
// User said no, use the other transport method
185+
transport = detectedTransport === 'http' ? 'sse' : 'http'
186+
const otherTransportDisplay = transport === 'http' ? 'streamable HTTP' : 'SSE'
187+
logger.info(`Installing as ${otherTransportDisplay} transport method.`)
188+
}
172189
} else {
173-
// Ask about legacy SSE (default no, but if they said no to HTTP, we need to confirm SSE)
174-
const usesLegacySSE = await logger.prompt('Does your server use the legacy SSE transport method?', {
175-
type: 'confirm',
176-
})
190+
// Detection failed, fall back to manual questions
191+
logger.info('Could not auto-detect transport type, please answer the following questions:')
177192

178-
if (usesLegacySSE) {
179-
transport = 'sse'
193+
// Ask about streamable HTTP first (default yes)
194+
const supportsStreamableHttp = await logger.prompt(
195+
'Does this server support the streamable HTTP transport method?',
196+
{ type: 'confirm' },
197+
)
198+
199+
if (supportsStreamableHttp) {
200+
transport = 'http'
180201
} else {
181-
logger.error('Server must support either streamable HTTP or legacy SSE transport method.')
182-
return
202+
// Ask about legacy SSE (default no, but if they said no to HTTP, we need to confirm SSE)
203+
const usesLegacySSE = await logger.prompt('Does your server use the legacy SSE transport method?', {
204+
type: 'confirm',
205+
})
206+
207+
if (usesLegacySSE) {
208+
transport = 'sse'
209+
} else {
210+
logger.error('Remote servers must support either streamable HTTP or legacy SSE transport method.')
211+
return
212+
}
183213
}
184214
}
185215
}

0 commit comments

Comments
 (0)