@@ -20,41 +20,108 @@ interface MCPToolDefinition {
2020
2121interface JSONRPCResponse {
2222 jsonrpc : string ;
23- id : number ;
23+ id : number | string | null ;
2424 result ?: {
2525 tools ?: MCPToolDefinition [ ] ;
2626 content ?: Array < { type : string ; text ?: string } > ;
2727 } ;
2828 error ?: { code : number ; message : string } ;
2929}
3030
31+ interface JSONRPCRequest {
32+ jsonrpc : "2.0" ;
33+ method : string ;
34+ params ?: Record < string , unknown > ;
35+ id ?: number ;
36+ }
37+
38+ const MCP_PROTOCOL_VERSION = "2025-03-26" ;
39+
3140let cachedSessionId : string | null = null ;
41+ let isInitialized = false ;
42+ let initializingPromise : Promise < void > | null = null ;
43+
44+ function parseSSEJSONResponse ( rawBody : string , method : string ) : JSONRPCResponse {
45+ const events = rawBody . split ( / \r ? \n \r ? \n / ) ;
46+
47+ for ( const eventBlock of events ) {
48+ const dataLines = eventBlock
49+ . split ( / \r ? \n / )
50+ . filter ( ( line ) => line . startsWith ( "data:" ) )
51+ . map ( ( line ) => line . slice ( 5 ) . trimStart ( ) )
52+ . filter ( Boolean ) ;
53+
54+ if ( dataLines . length === 0 ) {
55+ continue ;
56+ }
57+
58+ const payload = dataLines . join ( "\n" ) . trim ( ) ;
59+ if ( ! payload || payload === "[DONE]" ) {
60+ continue ;
61+ }
62+
63+ try {
64+ return JSON . parse ( payload ) as JSONRPCResponse ;
65+ } catch {
66+ // ignore non-JSON SSE frames and continue
67+ }
68+ }
69+
70+ throw new Error ( `Invalid MCP SSE response for "${ method } "` ) ;
71+ }
72+
73+ function parseMCPResponseBody (
74+ rawBody : string ,
75+ method : string ,
76+ contentType : string | null ,
77+ ) : JSONRPCResponse {
78+ if ( ! rawBody . trim ( ) ) {
79+ return { jsonrpc : "2.0" , id : null } ;
80+ }
81+
82+ if ( contentType ?. includes ( "text/event-stream" ) || rawBody . trimStart ( ) . startsWith ( "event:" ) ) {
83+ return parseSSEJSONResponse ( rawBody , method ) ;
84+ }
85+
86+ try {
87+ return JSON . parse ( rawBody ) as JSONRPCResponse ;
88+ } catch ( error ) {
89+ throw new Error ( `Invalid MCP JSON response for "${ method } ": ${ ( error as Error ) . message } ` ) ;
90+ }
91+ }
3292
3393async function mcpRequest (
3494 method : string ,
3595 params : Record < string , unknown > = { } ,
96+ options : { notification ?: boolean } = { } ,
3697) : Promise < JSONRPCResponse > {
3798 const url = process . env . SEED_DOCS_MCP_SERVER_URL ;
3899 if ( ! url ) throw new Error ( "SEED_DOCS_MCP_SERVER_URL is not set" ) ;
39100
40101 const headers : Record < string , string > = {
41102 "Content-Type" : "application/json" ,
42- Accept : "application/json" ,
103+ Accept : "application/json, text/event-stream" ,
104+ "MCP-Protocol-Version" : MCP_PROTOCOL_VERSION ,
43105 } ;
44106
45107 if ( cachedSessionId ) {
46108 headers [ "Mcp-Session-Id" ] = cachedSessionId ;
47109 }
48110
111+ const body : JSONRPCRequest = {
112+ jsonrpc : "2.0" ,
113+ method,
114+ params,
115+ } ;
116+
117+ if ( ! options . notification ) {
118+ body . id = Date . now ( ) ;
119+ }
120+
49121 const response = await fetch ( url , {
50122 method : "POST" ,
51123 headers,
52- body : JSON . stringify ( {
53- jsonrpc : "2.0" ,
54- id : Date . now ( ) ,
55- method,
56- params,
57- } ) ,
124+ body : JSON . stringify ( body ) ,
58125 } ) ;
59126
60127 // 세션 ID 저장
@@ -63,24 +130,55 @@ async function mcpRequest(
63130 cachedSessionId = sessionId ;
64131 }
65132
133+ const rawBody = await response . text ( ) ;
134+ const contentType = response . headers . get ( "content-type" ) ;
135+
66136 if ( ! response . ok ) {
67- throw new Error ( `MCP request failed: ${ response . status } ${ response . statusText } ` ) ;
137+ const details = rawBody . trim ( ) ;
138+ const suffix = details ? ` - ${ details . slice ( 0 , 300 ) } ` : "" ;
139+ throw new Error ( `MCP request failed: ${ response . status } ${ response . statusText } ${ suffix } ` ) ;
68140 }
69141
70- return response . json ( ) ;
142+ // notification 응답 혹은 빈 본문(202/204) 처리
143+ if ( options . notification || ! rawBody . trim ( ) ) {
144+ return { jsonrpc : "2.0" , id : null } ;
145+ }
146+
147+ return parseMCPResponseBody ( rawBody , method , contentType ) ;
148+ }
149+
150+ async function mcpNotification (
151+ method : string ,
152+ params : Record < string , unknown > = { } ,
153+ ) : Promise < void > {
154+ await mcpRequest ( method , params , { notification : true } ) ;
71155}
72156
73157/**
74158 * MCP 서버 초기화 (initialize + initialized 핸드셰이크)
75159 */
76160async function initializeMCP ( ) : Promise < void > {
77- await mcpRequest ( "initialize" , {
78- protocolVersion : "2025-03-26" ,
79- capabilities : { } ,
80- clientInfo : { name : "seed-docs-ai" , version : "1.0.0" } ,
161+ if ( isInitialized ) return ;
162+ if ( initializingPromise ) return initializingPromise ;
163+
164+ initializingPromise = ( async ( ) => {
165+ const initResponse = await mcpRequest ( "initialize" , {
166+ protocolVersion : MCP_PROTOCOL_VERSION ,
167+ capabilities : { } ,
168+ clientInfo : { name : "seed-docs-ai" , version : "1.0.0" } ,
169+ } ) ;
170+
171+ if ( initResponse . error ) {
172+ throw new Error ( `MCP initialize failed: ${ initResponse . error . message } ` ) ;
173+ }
174+
175+ await mcpNotification ( "notifications/initialized" ) ;
176+ isInitialized = true ;
177+ } ) ( ) . finally ( ( ) => {
178+ initializingPromise = null ;
81179 } ) ;
82180
83- await mcpRequest ( "notifications/initialized" ) ;
181+ return initializingPromise ;
84182}
85183
86184/**
0 commit comments