Skip to content

Commit c90df63

Browse files
committed
[CPL-20711] add http server transport option
1 parent e79efbe commit c90df63

File tree

7 files changed

+135
-14
lines changed

7 files changed

+135
-14
lines changed

src/env.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const {
1313
APP_VERSION,
1414
TRANSPORT,
1515
AUTHORIZATION_SERVER_HOST,
16+
PORT,
1617
} = parseEnv(process.env, {
1718
COUPLER_API_HOST: z.string().url().default('https://app.coupler.io'),
1819
COUPLER_ACCESS_TOKEN: z.string().trim().min(1),
@@ -21,5 +22,6 @@ export const {
2122
NODE_ENV: z.enum(ENVS).default('development'),
2223
APP_VERSION: z.string().trim().min(1),
2324
TRANSPORT: z.enum(TRANSPORTS).default('stdio'),
24-
AUTHORIZATION_SERVER_HOST: z.string().url().default('https://auth.coupler.io')
25+
AUTHORIZATION_SERVER_HOST: z.string().url().default('https://auth.coupler.io'),
26+
PORT: z.number().default(3001)
2527
})

src/index.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
2-
import { server } from '@/server'
3-
import { logger } from '@/logger'
1+
import { TRANSPORT } from '@/env'
2+
import { stdioServer } from '@/server/transports/stdio'
3+
import { httpServer } from '@/server/transports/http'
44

55
const main = () => {
6-
const transport = new StdioServerTransport()
7-
server.connect(transport)
8-
.then(() => {
9-
logger.info('Coupler.io MCP Server started')
10-
server.sendLoggingMessage({ level: 'info', data: 'Coupler.io MCP Server started' })
11-
})
12-
.catch((e) => {
13-
logger.error('Fatal error: ', e)
14-
process.exit(1)
15-
})
6+
const server = TRANSPORT === 'stdio' ? stdioServer : httpServer
7+
8+
server.start()
169
}
1710

1811
main()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import express from 'express'
2+
3+
import { PORT } from '@/env'
4+
import { loggingMiddleware, logger } from '@/server/logging'
5+
import { router as mcpRouter } from '@/server/transports/http/routes/mcp'
6+
import { router as oauthRouter } from '@/server/transports/http/routes/oauth'
7+
8+
const app = express()
9+
10+
app.disable('x-powered-by')
11+
12+
app.use(express.json())
13+
app.use(loggingMiddleware)
14+
15+
app.use('/mcp', mcpRouter)
16+
app.use('/oauth', oauthRouter)
17+
18+
export const httpServer = {
19+
start() {
20+
app.listen(PORT, () => {
21+
logger.info(`Coupler.io MCP Server listening on port ${PORT}`)
22+
})
23+
}
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Request, Response, NextFunction } from 'express'
2+
3+
export const authorize = (req: Request, res: Response, next: NextFunction): void => {
4+
const token = req.header('Authorization')?.split(/\s+/)[1]
5+
6+
if (!token) {
7+
res.status(401).json({ message: 'Unauthorized' })
8+
return
9+
}
10+
11+
next()
12+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Router } from 'express'
2+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
3+
4+
import { server } from '@/server/index'
5+
import { logger } from '@/server/logging'
6+
import { authorize } from '@/server/transports/http/middleware/authorize'
7+
8+
const methodNotAllowedResponse = {
9+
jsonrpc: '2.0',
10+
error: {
11+
code: -32000,
12+
message: 'Method not allowed.'
13+
},
14+
}
15+
16+
const router = Router()
17+
18+
router.use(authorize)
19+
20+
router.post('/', async (req, res) => {
21+
try {
22+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
23+
sessionIdGenerator: undefined,
24+
})
25+
26+
res.on('close', () => {
27+
transport.close()
28+
server.close()
29+
})
30+
31+
await server.connect(transport)
32+
await transport.handleRequest(req, res, req.body)
33+
} catch (error) {
34+
logger.error('Error handling MCP request:', error)
35+
if (!res.headersSent) {
36+
res.status(500).json({
37+
jsonrpc: '2.0',
38+
error: {
39+
code: -32603,
40+
message: 'Internal server error',
41+
},
42+
id: null,
43+
})
44+
}
45+
}
46+
})
47+
48+
router.use((req, res) => {
49+
if (req.method != 'POST') {
50+
res.status(405).json(methodNotAllowedResponse)
51+
}
52+
})
53+
54+
export { router }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { AUTHORIZATION_SERVER_HOST } from '@/env'
2+
import { Router } from 'express'
3+
4+
const router = Router()
5+
6+
const body = {
7+
registration_endpoint: `${AUTHORIZATION_SERVER_HOST}/oauth2/applications`,
8+
token_endpoint: `${AUTHORIZATION_SERVER_HOST}/oauth2/token`,
9+
authorization_endpoint: `${AUTHORIZATION_SERVER_HOST}/oauth2/authorize`
10+
}
11+
12+
router.get('/.well-known/oauth-authorization-server', (_req, res) => {
13+
res.json(body)
14+
})
15+
16+
export { router }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
2+
import { server } from '@/server/index'
3+
import { logger } from '@/server/logging'
4+
5+
export const stdioServer = {
6+
start() {
7+
const transport = new StdioServerTransport()
8+
server.connect(transport)
9+
.then(() => {
10+
logger.info('Coupler.io MCP Server started')
11+
server.sendLoggingMessage({ level: 'info', data: 'Coupler.io MCP Server started' })
12+
})
13+
.catch((e) => {
14+
logger.error('Fatal error: ', e)
15+
process.exit(1)
16+
})
17+
}
18+
}
19+
20+

0 commit comments

Comments
 (0)