@@ -3,6 +3,7 @@ import { eq, and, inArray } from 'drizzle-orm';
33import type { AnyDatabase } from '../db' ;
44import type { FastifyBaseLogger } from 'fastify' ;
55import { nanoid } from 'nanoid' ;
6+ import { generateInstancePath , generateInstanceToken } from '../utils/instancePathGenerator' ;
67
78/**
89 * Instance with user information (includes user_slug and user_email from authUser table)
@@ -13,6 +14,7 @@ export interface InstanceWithUser {
1314 user_id : string ;
1415 user_slug : string ;
1516 user_email : string ;
17+ instance_path : string | null ;
1618 status : string ;
1719 status_message : string | null ;
1820 status_updated_at : Date | null ;
@@ -42,37 +44,77 @@ export class McpInstanceService {
4244 * @param installationId - Installation ID
4345 * @param userId - User ID
4446 * @param status - Initial status (default: 'provisioning')
47+ * @returns Object with instanceId, instancePath, and instanceToken (plaintext)
4548 */
4649 async createInstance (
4750 installationId : string ,
4851 userId : string ,
4952 status : string = 'provisioning' ,
5053 statusMessage ?: string
51- ) : Promise < void > {
54+ ) : Promise < { instanceId : string ; instancePath : string ; instanceToken : string } > {
5255 const schema = await import ( '../db/schema' ) ;
5356 const { mcpServerInstances } = schema ;
5457
5558 const instanceId = `inst_${ nanoid ( ) } ` ;
5659
57- await this . db . insert ( mcpServerInstances ) . values ( {
58- id : instanceId ,
59- installation_id : installationId ,
60- user_id : userId ,
61- status,
62- status_message : statusMessage ,
63- status_updated_at : new Date ( ) ,
64- created_at : new Date ( ) ,
65- updated_at : new Date ( ) ,
66- } ) ;
60+ const MAX_PATH_RETRIES = 5 ;
61+ let instancePath = '' ;
62+ let instanceToken : string ;
63+ let instanceTokenHash : string ;
64+ let inserted = false ;
65+
66+ // Generate token once (outside retry loop)
67+ const tokenResult = await generateInstanceToken ( ) ;
68+ instanceToken = tokenResult . plaintext ;
69+ instanceTokenHash = tokenResult . hash ;
70+
71+ for ( let attempt = 0 ; attempt < MAX_PATH_RETRIES ; attempt ++ ) {
72+ instancePath = generateInstancePath ( ) ;
73+
74+ try {
75+ await this . db . insert ( mcpServerInstances ) . values ( {
76+ id : instanceId ,
77+ installation_id : installationId ,
78+ user_id : userId ,
79+ instance_path : instancePath ,
80+ instance_token : instanceTokenHash , // Store hash, not plaintext
81+ status,
82+ status_message : statusMessage ,
83+ status_updated_at : new Date ( ) ,
84+ created_at : new Date ( ) ,
85+ updated_at : new Date ( ) ,
86+ } ) ;
87+ inserted = true ;
88+ break ;
89+ } catch ( error : any ) {
90+ // Check if it's a unique constraint violation on instance_path
91+ if ( error . code === '23505' && error . constraint ?. includes ( 'instance_path' ) ) {
92+ this . logger . warn ( {
93+ attempt : attempt + 1 ,
94+ instancePath
95+ } , 'Instance path collision detected, retrying with new path' ) ;
96+ continue ;
97+ }
98+ // Re-throw non-uniqueness errors
99+ throw error ;
100+ }
101+ }
102+
103+ if ( ! inserted ) {
104+ throw new Error ( `Failed to generate unique instance path after ${ MAX_PATH_RETRIES } retries` ) ;
105+ }
67106
68107 this . logger . info ( {
69108 operation : 'create_instance' ,
70109 instanceId,
71110 installationId,
72111 userId,
112+ instancePath,
73113 status,
74114 statusMessage
75- } , 'Created MCP server instance' ) ;
115+ } , 'Created MCP server instance with path-based access' ) ;
116+
117+ return { instanceId, instancePath, instanceToken } ;
76118 }
77119
78120 /**
@@ -250,6 +292,7 @@ export class McpInstanceService {
250292 user_id : mcpServerInstances . user_id ,
251293 user_slug : authUser . username ,
252294 user_email : authUser . email ,
295+ instance_path : mcpServerInstances . instance_path ,
253296 status : mcpServerInstances . status ,
254297 status_message : mcpServerInstances . status_message ,
255298 status_updated_at : mcpServerInstances . status_updated_at ,
0 commit comments