Skip to content

Commit 34d1a79

Browse files
committed
feat(all): add satellite selection to MCP client configuration
- Add satellite selection dropdown to dashboard modal and client-configuration page - Replace hardcoded satellite URL with dynamic database lookup - Filter to show only active satellites (status='active') - Auto-select first satellite when only one is available - Implement 50/50 grid layout with client on left, satellite on right - Add Alert component for no satellites available state - Backend: Add getSatelliteUrl() helper with active satellite filtering - Backend: Update generateClientConfig() to accept satelliteUrl parameter - Backend: Add satelliteId query parameter support to config endpoints - Frontend: Add loadAvailableSatellites() with TeamService integration - Frontend: Add satellite selection watchers for dynamic config reload - Fix parameter order in client-configuration loadConfiguration() Close #548
1 parent af13d14 commit 34d1a79

File tree

8 files changed

+418
-40
lines changed

8 files changed

+418
-40
lines changed

services/backend/api-spec.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5038,6 +5038,15 @@
50385038
],
50395039
"description": "Returns configuration actions filtered by category (connection, ai-instructions) for the specified MCP client.",
50405040
"parameters": [
5041+
{
5042+
"schema": {
5043+
"type": "string"
5044+
},
5045+
"in": "query",
5046+
"name": "satelliteId",
5047+
"required": false,
5048+
"description": "Satellite ID to generate configuration for (optional, defaults to first active satellite)"
5049+
},
50415050
{
50425051
"schema": {
50435052
"type": "string"
@@ -5377,6 +5386,15 @@
53775386
],
53785387
"description": "Returns all configuration actions for the specified MCP client. Consider using /config/:category/:client for filtered results.",
53795388
"parameters": [
5389+
{
5390+
"schema": {
5391+
"type": "string"
5392+
},
5393+
"in": "query",
5394+
"name": "satelliteId",
5395+
"required": false,
5396+
"description": "Satellite ID to generate configuration for (optional, defaults to first active satellite)"
5397+
},
53805398
{
53815399
"schema": {
53825400
"type": "string",

services/backend/api-spec.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3480,6 +3480,13 @@ paths:
34803480
description: Returns configuration actions filtered by category (connection,
34813481
ai-instructions) for the specified MCP client.
34823482
parameters:
3483+
- schema:
3484+
type: string
3485+
in: query
3486+
name: satelliteId
3487+
required: false
3488+
description: Satellite ID to generate configuration for (optional, defaults to
3489+
first active satellite)
34833490
- schema:
34843491
type: string
34853492
in: path
@@ -3702,6 +3709,13 @@ paths:
37023709
description: Returns all configuration actions for the specified MCP client.
37033710
Consider using /config/:category/:client for filtered results.
37043711
parameters:
3712+
- schema:
3713+
type: string
3714+
in: query
3715+
name: satelliteId
3716+
required: false
3717+
description: Satellite ID to generate configuration for (optional, defaults to
3718+
first active satellite)
37053719
- schema:
37063720
type: string
37073721
enum:

services/backend/src/routes/users/satellite/clients.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export default async function listClients(server: FastifyInstance) {
5454
// Loop through all client types and extract their categories
5555
for (const client of CLIENT_TYPES) {
5656
try {
57-
const actions = generateClientConfig(client.id);
57+
// Use placeholder URL since we're only discovering categories, not generating actual configs
58+
const actions = await generateClientConfig(client.id, 'https://placeholder.satellite.url');
5859
server.log.debug({ clientId: client.id, actionsCount: actions.length }, 'Generated client config');
5960

6061
// Extract unique categories from this client's actions

services/backend/src/routes/users/satellite/config.ts

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { type FastifyInstance } from 'fastify';
22
import { requireAuthenticationAny, requireOAuthScope } from '../../../middleware/oauthMiddleware';
3+
import { getDb, getSchema } from '../../../db';
4+
import { eq, and, or, isNull } from 'drizzle-orm';
35
import fs from 'fs';
46
import path from 'path';
57
import {
68
CLIENT_PARAM_SCHEMA,
79
CLIENT_CATEGORY_PARAM_SCHEMA,
10+
SATELLITE_ID_QUERY_SCHEMA,
811
SUCCESS_RESPONSE_SCHEMA,
912
ERROR_RESPONSE_SCHEMA,
1013
type ClientParams,
1114
type ClientCategoryParams,
15+
type SatelliteIdQuery,
1216
type ErrorResponse,
1317
type JsonAction,
1418
type LinkAction,
@@ -23,9 +27,44 @@ function createBase64Config(config: object): string {
2327
return Buffer.from(JSON.stringify(config)).toString('base64');
2428
}
2529

30+
// Helper function to get satellite URL from database
31+
async function getSatelliteUrl(
32+
satelliteId: string | undefined,
33+
teamId: string,
34+
db: ReturnType<typeof getDb>,
35+
satellites: ReturnType<typeof getSchema>['satellites']
36+
): Promise<string> {
37+
// Build query condition - if satelliteId provided, use it; otherwise get first active satellite
38+
const query = satelliteId
39+
? eq(satellites.id, satelliteId)
40+
: or(
41+
and(eq(satellites.satellite_type, 'global'), isNull(satellites.team_id)),
42+
and(eq(satellites.satellite_type, 'team'), eq(satellites.team_id, teamId))
43+
);
44+
45+
const satelliteRecords = await db
46+
.select({ satellite_url: satellites.satellite_url })
47+
.from(satellites)
48+
.where(
49+
and(
50+
eq(satellites.status, 'active'), // CRITICAL: Only active satellites
51+
query
52+
)
53+
)
54+
.limit(1);
55+
56+
if (satelliteRecords.length === 0) {
57+
throw new Error('No active satellites available');
58+
}
59+
60+
return satelliteRecords[0].satellite_url;
61+
}
62+
2663
// Client configuration generator - now returns array of actions
27-
export function generateClientConfig(clientType: string): ClientConfigResponse {
28-
const satelliteUrl = 'https://satellite.deploystack.io';
64+
export async function generateClientConfig(
65+
clientType: string,
66+
satelliteUrl: string
67+
): Promise<ClientConfigResponse> {
2968
const actions: ClientConfigResponse = [];
3069

3170
// Read AI instruction files from local directory
@@ -239,7 +278,10 @@ export function generateClientConfig(clientType: string): ClientConfigResponse {
239278

240279
export default async function getClientConfig(server: FastifyInstance) {
241280
// New route with category filtering
242-
server.get('/me/satellite/config/:category/:client', {
281+
server.get<{
282+
Params: ClientCategoryParams;
283+
Querystring: SatelliteIdQuery;
284+
}>('/me/satellite/config/:category/:client', {
243285
preValidation: [
244286
requireAuthenticationAny(),
245287
requireOAuthScope('mcp:read')
@@ -253,6 +295,7 @@ export default async function getClientConfig(server: FastifyInstance) {
253295
{ bearerAuth: [] }
254296
],
255297
params: CLIENT_CATEGORY_PARAM_SCHEMA,
298+
querystring: SATELLITE_ID_QUERY_SCHEMA,
256299
response: {
257300
200: {
258301
...SUCCESS_RESPONSE_SCHEMA,
@@ -275,9 +318,35 @@ export default async function getClientConfig(server: FastifyInstance) {
275318
}, async (request, reply) => {
276319
try {
277320
const { category, client } = request.params as ClientCategoryParams;
321+
const { satelliteId } = request.query as SatelliteIdQuery;
322+
const userId = request.user!.id;
323+
324+
const db = getDb();
325+
const { satellites, teams } = getSchema();
326+
327+
// Get user's default team
328+
const defaultTeam = await db
329+
.select({ id: teams.id })
330+
.from(teams)
331+
.where(
332+
and(
333+
eq(teams.owner_id, userId),
334+
eq(teams.is_default, true)
335+
)
336+
)
337+
.limit(1);
338+
339+
if (defaultTeam.length === 0) {
340+
throw new Error('No default team found for user');
341+
}
342+
343+
const teamId = defaultTeam[0].id;
344+
345+
// Get satellite URL from database
346+
const satelliteUrl = await getSatelliteUrl(satelliteId, teamId, db, satellites);
278347

279-
// Generate all client-specific configuration actions
280-
const allActions = generateClientConfig(client);
348+
// Generate all client-specific configuration actions with dynamic URL
349+
const allActions = await generateClientConfig(client, satelliteUrl);
281350

282351
// Filter by category
283352
const filteredActions = allActions.filter(action => action.category === category);
@@ -295,7 +364,10 @@ export default async function getClientConfig(server: FastifyInstance) {
295364
});
296365

297366
// Keep legacy route for backward compatibility (returns all actions)
298-
server.get('/me/satellite/config/:client', {
367+
server.get<{
368+
Params: ClientParams;
369+
Querystring: SatelliteIdQuery;
370+
}>('/me/satellite/config/:client', {
299371
preValidation: [
300372
requireAuthenticationAny(),
301373
requireOAuthScope('mcp:read')
@@ -309,6 +381,7 @@ export default async function getClientConfig(server: FastifyInstance) {
309381
{ bearerAuth: [] }
310382
],
311383
params: CLIENT_PARAM_SCHEMA,
384+
querystring: SATELLITE_ID_QUERY_SCHEMA,
312385
response: {
313386
200: {
314387
...SUCCESS_RESPONSE_SCHEMA,
@@ -331,9 +404,35 @@ export default async function getClientConfig(server: FastifyInstance) {
331404
}, async (request, reply) => {
332405
try {
333406
const { client } = request.params as ClientParams;
407+
const { satelliteId } = request.query as SatelliteIdQuery;
408+
const userId = request.user!.id;
409+
410+
const db = getDb();
411+
const { satellites, teams } = getSchema();
412+
413+
// Get user's default team
414+
const defaultTeam = await db
415+
.select({ id: teams.id })
416+
.from(teams)
417+
.where(
418+
and(
419+
eq(teams.owner_id, userId),
420+
eq(teams.is_default, true)
421+
)
422+
)
423+
.limit(1);
424+
425+
if (defaultTeam.length === 0) {
426+
throw new Error('No default team found for user');
427+
}
428+
429+
const teamId = defaultTeam[0].id;
430+
431+
// Get satellite URL from database
432+
const satelliteUrl = await getSatelliteUrl(satelliteId, teamId, db, satellites);
334433

335-
// Generate client-specific configuration actions
336-
const configActions = generateClientConfig(client);
434+
// Generate client-specific configuration actions with dynamic URL
435+
const configActions = await generateClientConfig(client, satelliteUrl);
337436

338437
const jsonString = JSON.stringify(configActions);
339438
return reply.status(200).type('application/json').send(jsonString);

services/backend/src/routes/users/satellite/schemas.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ export const CLIENT_CATEGORY_PARAM_SCHEMA = {
8080
additionalProperties: false
8181
} as const;
8282

83+
export const SATELLITE_ID_QUERY_SCHEMA = {
84+
type: 'object',
85+
properties: {
86+
satelliteId: {
87+
type: 'string',
88+
description: 'Satellite ID to generate configuration for (optional, defaults to first active satellite)'
89+
}
90+
},
91+
additionalProperties: false
92+
} as const;
93+
8394
// New response schema for array-based configuration with actions
8495
export const SUCCESS_RESPONSE_SCHEMA = {
8596
type: 'array',
@@ -236,6 +247,10 @@ export interface ClientCategoryParams {
236247
client: ClientType;
237248
}
238249

250+
export interface SatelliteIdQuery {
251+
satelliteId?: string;
252+
}
253+
239254
export interface ClientsListResponse {
240255
categories: readonly ClientCategory[];
241256
}

0 commit comments

Comments
 (0)