@@ -17,7 +17,7 @@ module.exports = async function (app) {
1717
1818 app . addHook ( 'preHandler' , app . verifySession )
1919 app . addHook ( 'preHandler' , async ( request , reply ) => {
20- if ( ! serviceEnabled ) {
20+ if ( ! serviceEnabled || ! expertUrl ) {
2121 return reply . code ( 501 ) . send ( {
2222 code : 'service_disabled' ,
2323 error : 'Expert service is not enabled'
@@ -38,11 +38,24 @@ module.exports = async function (app) {
3838 // Get the user object
3939 request . user = await app . db . models . User . byId ( request . session . User . id )
4040 if ( ! request . user ) {
41- reply . code ( 401 ) . send ( {
41+ return reply . code ( 401 ) . send ( {
4242 code : 'unauthorized' ,
4343 error : 'unauthorized'
4444 } )
4545 }
46+ // Ensure users team access is valid
47+ const teamId = request . body . context ?. teamId // `context.teamId` is the hash provided in the body context by the client
48+ if ( ! teamId ) {
49+ return reply . status ( 404 ) . send ( { code : 'not_found' , error : 'Not Found' } )
50+ }
51+ const existingRole = await request . user . getTeamMembership ( teamId )
52+ if ( ! existingRole ) {
53+ return reply . status ( 404 ) . send ( { code : 'not_found' , error : 'Not Found' } )
54+ }
55+ request . team = await app . db . models . Team . byId ( teamId )
56+ if ( ! request . team ) {
57+ return reply . status ( 404 ) . send ( { code : 'not_found' , error : 'Not Found' } )
58+ }
4659 }
4760 } )
4861
@@ -104,9 +117,164 @@ module.exports = async function (app) {
104117 throw new Error ( 'Transaction ID mismatch' )
105118 }
106119
120+ reply . send ( response . data )
121+ } catch ( error ) {
122+ reply . code ( error . response ?. status || 500 ) . send ( { code : error . response ?. data ?. code || 'unexpected_error' , error : error . response ?. data ?. error || error . message } )
123+ }
124+ } )
125+ /**
126+ * an endpoint to retrieve MCP features (prompts/resources/tools) for the users team
127+ */
128+ app . post ( '/mcp/features' , {
129+ schema : {
130+ hide : true , // dont show in swagger
131+ headers : {
132+ type : 'object' ,
133+ properties : {
134+ 'x-chat-transaction-id' : { type : 'string' , minLength : 1 }
135+ } ,
136+ required : [ 'x-chat-transaction-id' ]
137+ } ,
138+ body : {
139+ type : 'object' ,
140+ properties : {
141+ context : {
142+ type : 'object' ,
143+ properties : {
144+ teamId : { type : 'string' , minLength : 10 }
145+ } ,
146+ required : [ 'teamId' ] ,
147+ additionalProperties : true
148+ }
149+ } ,
150+ required : [ 'context' ]
151+ } ,
152+ response : {
153+ 200 : {
154+ type : 'object' ,
155+ properties : {
156+ transactionId : { type : 'string' } ,
157+ servers : {
158+ type : 'array' ,
159+ items : {
160+ type : 'object' ,
161+ properties : {
162+ team : { type : 'string' } ,
163+ instance : { type : 'string' } ,
164+ instanceType : { type : 'string' , enum : [ 'instance' , 'device' ] } ,
165+ instanceName : { type : 'string' } ,
166+ mcpServerName : { type : 'string' } ,
167+ prompts : { type : 'array' , items : { type : 'object' , additionalProperties : true } } ,
168+ resources : { type : 'array' , items : { type : 'object' , additionalProperties : true } } ,
169+ resourceTemplates : { type : 'array' , items : { type : 'object' , additionalProperties : true } } ,
170+ tools : { type : 'array' , items : { type : 'object' , additionalProperties : true } } ,
171+ mcpProtocol : { type : 'string' , enum : [ 'http' , 'sse' ] } ,
172+ mcpServerUrl : { type : 'string' } ,
173+ title : { type : 'string' } ,
174+ version : { type : 'string' } ,
175+ description : { type : 'string' }
176+ } ,
177+ required : [ 'instance' , 'instanceType' , 'instanceName' , 'mcpServerName' , 'prompts' , 'resources' , 'resourceTemplates' , 'tools' , 'mcpProtocol' ] ,
178+ additionalProperties : false
179+ }
180+ }
181+ } ,
182+ additionalProperties : false
183+ } ,
184+ '4xx' : {
185+ $ref : 'APIError'
186+ }
187+ }
188+ }
189+ } ,
190+ /**
191+ * Get MCP capabilities for the user's team
192+ * @param {import('fastify').FastifyRequest } request
193+ * @param {import('fastify').FastifyReply } reply
194+ */
195+ async ( request , reply ) => {
196+ try {
197+ /** @type {MCPServerItem[] } */
198+ const runningInstancesWithMCPServer = [ ]
199+ const transactionId = request . headers [ 'x-chat-transaction-id' ]
200+ const mcpCapabilitiesUrl = `${ expertUrl . split ( '/' ) . slice ( 0 , - 1 ) . join ( '/' ) } /mcp/features`
201+ const mcpServers = await app . db . models . MCPRegistration . byTeam ( request . team . id , { includeInstance : true } ) || [ ]
202+
203+ for ( const server of mcpServers ) {
204+ const { name, protocol, endpointRoute, TeamId, Project, Device, title, version, description } = server
205+ if ( TeamId !== request . team . id ) {
206+ // shouldn't happen due to byTeam filter, but just in case
207+ continue
208+ }
209+ let owner , ownerId , ownerType
210+ if ( Device ) {
211+ ownerType = 'device'
212+ owner = Device
213+ ownerId = Device . hashid
214+ } else if ( Project ) {
215+ ownerType = 'instance'
216+ owner = Project
217+ ownerId = Project . id
218+ } else {
219+ continue
220+ }
221+
222+ const liveState = await owner . liveState ( { omitStorageFlows : true } )
223+ if ( liveState ?. meta ?. state !== 'running' ) {
224+ continue
225+ }
226+
227+ runningInstancesWithMCPServer . push ( {
228+ team : request . team . hashid ,
229+ instance : ownerId ,
230+ instanceType : ownerType ,
231+ instanceName : owner . name ,
232+ instanceUrl : owner . url ,
233+ mcpServerName : name ,
234+ mcpEndpoint : endpointRoute ,
235+ mcpProtocol : protocol ,
236+ title,
237+ version,
238+ description
239+ } )
240+ }
241+ if ( runningInstancesWithMCPServer . length === 0 ) {
242+ return reply . send ( { servers : [ ] , transactionId } )
243+ }
244+ const response = await axios . post ( mcpCapabilitiesUrl , {
245+ teamId : request . team . hashid ,
246+ servers : runningInstancesWithMCPServer
247+ } , {
248+ headers : {
249+ Origin : request . headers . origin ,
250+ 'X-Chat-Transaction-ID' : transactionId ,
251+ ...( serviceToken ? { Authorization : `Bearer ${ serviceToken } ` } : { } )
252+ } ,
253+ timeout : requestTimeout
254+ } )
255+
256+ if ( response . data . transactionId !== transactionId ) {
257+ throw new Error ( 'Transaction ID mismatch' )
258+ }
259+
107260 reply . send ( response . data )
108261 } catch ( error ) {
109262 reply . code ( error . response ?. status || 500 ) . send ( { code : error . response ?. data ?. code || 'unexpected_error' , error : error . response ?. data ?. error || error . message } )
110263 }
111264 } )
112265}
266+
267+ /**
268+ * @typedef {Object } MCPServerItem MCP server info for a team
269+ * @property {string } team
270+ * @property {string } instance
271+ * @property {string } instanceType
272+ * @property {string } instanceName
273+ * @property {string } instanceUrl
274+ * @property {string } mcpServerName
275+ * @property {string } mcpEndpoint
276+ * @property {string } mcpProtocol
277+ * @property {string } [title]
278+ * @property {string } [version]
279+ * @property {string } [description]
280+ */
0 commit comments