Skip to content

Commit 5793cfe

Browse files
committed
feat(satellite): enhance MCP activity tracking with instance token support
1 parent 6dfd20e commit 5793cfe

File tree

9 files changed

+167
-68
lines changed

9 files changed

+167
-68
lines changed

services/backend/src/db/schema-tables/mcp-activity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const mcpClientActivity = pgTable('mcpClientActivity', {
1616
satellite_id: text('satellite_id').notNull().references(() => satellites.id, { onDelete: 'cascade' }),
1717

1818
// Authentication (extensible for future)
19-
auth_type: text('auth_type', { enum: ['oauth', 'api_key'] }).notNull(),
19+
auth_type: text('auth_type', { enum: ['oauth', 'api_key', 'instance_token'] }).notNull(),
2020
oauth_client_id: text('oauth_client_id'), // References dynamicOauthClients.client_id
2121
api_key_id: text('api_key_id'), // Future: references to API key table
2222
auth_identifier: text('auth_identifier').notNull(), // Computed: 'oauth:{client_id}' or 'apikey:{key_id}'

services/backend/src/events/satellite/mcp-client-activity.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,19 @@ export const SCHEMA = {
1919
minLength: 1,
2020
description: 'Team ID in which the requests were made'
2121
},
22-
oauth_client_id: {
23-
type: 'string',
22+
oauth_client_id: {
23+
type: 'string',
2424
minLength: 1,
25-
description: 'OAuth client ID of the MCP client'
25+
description: 'OAuth client ID or installation ID of the MCP client'
26+
},
27+
auth_type: {
28+
type: 'string',
29+
enum: ['oauth', 'instance_token'],
30+
description: 'Authentication method used by the MCP client'
31+
},
32+
auth_identifier: {
33+
type: 'string',
34+
description: 'Pre-computed auth identifier (e.g. instance:{installation_id})'
2635
},
2736
client_name: {
2837
type: 'string',
@@ -57,11 +66,10 @@ export const SCHEMA = {
5766
}
5867
},
5968
required: [
60-
'user_id',
61-
'team_id',
62-
'oauth_client_id',
63-
'request_count',
64-
'tool_call_count',
69+
'user_id',
70+
'team_id',
71+
'request_count',
72+
'tool_call_count',
6573
'last_activity_at'
6674
],
6775
additionalProperties: true
@@ -70,7 +78,9 @@ export const SCHEMA = {
7078
interface McpClientActivityData {
7179
user_id: string;
7280
team_id: string;
73-
oauth_client_id: string;
81+
oauth_client_id?: string;
82+
auth_type?: 'oauth' | 'instance_token';
83+
auth_identifier?: string;
7484
client_name?: string;
7585
user_agent?: string;
7686
ip_address?: string;
@@ -96,6 +106,7 @@ async function updateCumulativeActivity(
96106
satelliteId: string,
97107
data: McpClientActivityData,
98108
authIdentifier: string,
109+
authType: 'oauth' | 'api_key' | 'instance_token',
99110
activityTimestamp: Date,
100111
eventTimestamp: Date
101112
): Promise<void> {
@@ -127,8 +138,8 @@ async function updateCumulativeActivity(
127138
user_id: data.user_id,
128139
team_id: data.team_id,
129140
satellite_id: satelliteId,
130-
auth_type: 'oauth',
131-
oauth_client_id: data.oauth_client_id,
141+
auth_type: authType,
142+
oauth_client_id: data.oauth_client_id || null,
132143
api_key_id: null,
133144
auth_identifier: authIdentifier,
134145
client_name: data.client_name || null,
@@ -197,14 +208,16 @@ export async function handle(
197208
logger: FastifyBaseLogger
198209
): Promise<void> {
199210
const data = eventData as unknown as McpClientActivityData;
200-
const authIdentifier = `oauth:${data.oauth_client_id}`;
211+
const authType = data.auth_type || 'oauth';
212+
const authIdentifier = data.auth_identifier || `oauth:${data.oauth_client_id}`;
201213
const activityTimestamp = new Date(data.last_activity_at);
202214

203215
await updateCumulativeActivity(
204216
db,
205217
satelliteId,
206218
data,
207219
authIdentifier,
220+
authType,
208221
activityTimestamp,
209222
eventTimestamp
210223
);

services/satellite/src/core/instance-router.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { McpToolExecutor } from '../lib/mcp-tool-executor';
1111
import { McpSessionManager } from '../lib/mcp-session-manager';
1212
import { UnifiedToolDiscoveryManager } from '../services/unified-tool-discovery-manager';
1313
import { DynamicConfigManager } from '../services/dynamic-config-manager';
14+
import { McpActivityTracker } from '../services/mcp-activity-tracker';
15+
import { trackMcpActivity } from '../services/activity-tracking-helper';
1416
import { ProcessManager } from '../process';
1517
import { McpServerConfig } from '../services/command-polling-service';
1618

@@ -40,6 +42,7 @@ export class InstanceRouter {
4042
private configManager: DynamicConfigManager;
4143
private toolDiscoveryManager: UnifiedToolDiscoveryManager;
4244
private processManager: ProcessManager;
45+
private activityTracker?: McpActivityTracker;
4346

4447
constructor(deps: {
4548
logger: FastifyBaseLogger;
@@ -48,13 +51,15 @@ export class InstanceRouter {
4851
configManager: DynamicConfigManager;
4952
toolDiscoveryManager: UnifiedToolDiscoveryManager;
5053
processManager: ProcessManager;
54+
activityTracker?: McpActivityTracker;
5155
}) {
5256
this.logger = deps.logger.child({ component: 'InstanceRouter' });
5357
this.toolExecutor = deps.toolExecutor;
5458
this.sessionManager = deps.sessionManager;
5559
this.configManager = deps.configManager;
5660
this.toolDiscoveryManager = deps.toolDiscoveryManager;
5761
this.processManager = deps.processManager;
62+
this.activityTracker = deps.activityTracker;
5863
}
5964

6065
/**
@@ -88,6 +93,10 @@ export class InstanceRouter {
8893
// Register tools/call handler - execute on THIS instance
8994
server.setRequestHandler(CallToolRequestSchema, async (request) => {
9095
const { name: toolName, arguments: toolArgs } = request.params;
96+
const startTime = Date.now();
97+
let result: any;
98+
let success = true;
99+
let errorMessage: string | undefined;
91100

92101
// Look up the tool in the discovery cache to get its correct namespacedName.
93102
// processId is the installation name (e.g., "duckduckgo-mcp-server-john-plhdo1j4kuit0et-..."),
@@ -117,13 +126,40 @@ export class InstanceRouter {
117126
namespaced_name: matchedTool.namespacedName
118127
}, `Executing tool ${toolName} on instance ${processId}`);
119128

120-
const result = await this.toolExecutor.executeToolCall(
121-
matchedTool.namespacedName, // e.g., "duckduckgo-mcp-server:search"
122-
toolArgs || {},
123-
processId // Force routing to this specific process
124-
);
129+
try {
130+
result = await this.toolExecutor.executeToolCall(
131+
matchedTool.namespacedName, // e.g., "duckduckgo-mcp-server:search"
132+
toolArgs || {},
133+
processId // Force routing to this specific process
134+
);
125135

126-
return result;
136+
return result;
137+
} catch (error) {
138+
success = false;
139+
errorMessage = error instanceof Error ? error.message : String(error);
140+
throw error;
141+
} finally {
142+
const responseTimeMs = Date.now() - startTime;
143+
144+
// Buffer request log (mirrors mcp-server-wrapper.ts finally block)
145+
const serverConfig = this.configManager.getMcpServerConfig(processId);
146+
const loggingEnabled = serverConfig?.settings?.request_logging_enabled !== false;
147+
148+
if (serverConfig?.installation_id && serverConfig?.team_id && loggingEnabled) {
149+
this.toolExecutor.bufferRequestLogEntry({
150+
installation_id: serverConfig.installation_id,
151+
team_id: serverConfig.team_id,
152+
user_id: serverConfig.user_id,
153+
tool_name: toolName,
154+
tool_params: toolArgs || {},
155+
tool_response: result,
156+
response_time_ms: responseTimeMs,
157+
success,
158+
error_message: errorMessage,
159+
timestamp: new Date().toISOString()
160+
});
161+
}
162+
}
127163
});
128164

129165
this.logger.info({
@@ -259,6 +295,16 @@ export class InstanceRouter {
259295
instance_path: instancePath,
260296
process_id: instanceConfig.processId
261297
}, 'Instance authentication successful');
298+
299+
// Track activity for personal dashboard (same pipeline as OAuth auth)
300+
if (this.activityTracker && instanceConfig.config.user_id && instanceConfig.config.team_id) {
301+
trackMcpActivity(this.activityTracker, request, {
302+
userId: instanceConfig.config.user_id,
303+
teamId: instanceConfig.config.team_id,
304+
authIdentifier: instanceConfig.config.installation_id || instanceConfig.processId,
305+
authType: 'instance_token',
306+
}, this.logger);
307+
}
262308
};
263309

264310
// POST /i/:instancePath/mcp - Client-to-server MCP messages

services/satellite/src/events/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export interface EventDataMap {
5757
user_id: string;
5858
team_id: string;
5959
oauth_client_id: string;
60+
auth_type?: 'oauth' | 'instance_token';
61+
auth_identifier?: string;
6062
client_name: string;
6163
user_agent: string;
6264
ip_address: string;

services/satellite/src/jobs/mcp-activity-report-job.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export class McpActivityReportJob extends BaseJob {
6363
user_id: activity.userId,
6464
team_id: activity.teamId,
6565
oauth_client_id: activity.oauthClientId,
66+
auth_type: activity.authType,
67+
auth_identifier: activity.authType === 'instance_token'
68+
? `instance:${activity.oauthClientId}`
69+
: undefined,
6670
client_name: activity.clientName,
6771
user_agent: activity.userAgent,
6872
ip_address: activity.ipAddress,

services/satellite/src/middleware/auth-middleware.ts

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FastifyRequest, FastifyReply } from 'fastify';
22
import { TokenIntrospectionService, TokenValidationResult } from '../services/token-introspection-service';
33
import { McpActivityTracker } from '../services/mcp-activity-tracker';
4-
import { deriveClientName, extractSessionId, extractIpAddress } from '../services/client-name-detector';
4+
import { trackMcpActivity } from '../services/activity-tracking-helper';
55

66
// Extend FastifyRequest to include authentication context
77
declare module 'fastify' {
@@ -85,51 +85,12 @@ export function requireAuthentication(
8585

8686
// Track MCP client activity for personal dashboard (if tracker provided)
8787
if (activityTracker && request.auth.client_id) {
88-
try {
89-
const clientName = deriveClientName(request.headers as Record<string, string | string[] | undefined>);
90-
const sessionId = extractSessionId(request.headers as Record<string, string | string[] | undefined>);
91-
const ipAddress = extractIpAddress(
92-
request.headers as Record<string, string | string[] | undefined>,
93-
request.socket.remoteAddress
94-
);
95-
const userAgent = request.headers['user-agent'] || 'unknown';
96-
97-
// Check if this is a tool call (mcp.tool.executed)
98-
// This is a simple heuristic - more sophisticated detection can be added later
99-
let isToolCall = false;
100-
if (request.url.includes('tools/call')) {
101-
isToolCall = true;
102-
} else if (request.body && typeof request.body === 'object' && 'method' in request.body) {
103-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
104-
isToolCall = (request.body as any).method === 'tools/call';
105-
}
106-
107-
activityTracker.trackRequest(
108-
request.auth.user.id,
109-
request.auth.team.id,
110-
request.auth.client_id,
111-
clientName,
112-
userAgent,
113-
ipAddress,
114-
sessionId,
115-
isToolCall
116-
);
117-
118-
request.log.debug({
119-
operation: 'activity_tracked',
120-
userId: request.auth.user.id,
121-
teamId: request.auth.team.id,
122-
clientId: request.auth.client_id,
123-
clientName,
124-
isToolCall
125-
}, 'MCP client activity tracked');
126-
} catch (error) {
127-
// Activity tracking failure is non-fatal
128-
request.log.warn({
129-
operation: 'activity_tracking_failed',
130-
error: error instanceof Error ? error.message : String(error)
131-
}, 'Failed to track MCP client activity (non-fatal)');
132-
}
88+
trackMcpActivity(activityTracker, request, {
89+
userId: request.auth.user.id,
90+
teamId: request.auth.team.id,
91+
authIdentifier: request.auth.client_id,
92+
authType: 'oauth',
93+
}, request.log);
13394
}
13495

13596
} catch (error) {

services/satellite/src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,8 @@ export async function createServer() {
12671267
sessionManager: instanceSessionManager, // Separate session manager
12681268
configManager: dynamicConfigManager,
12691269
toolDiscoveryManager,
1270-
processManager
1270+
processManager,
1271+
activityTracker,
12711272
});
12721273

12731274
// Setup instance router routes
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { FastifyRequest, FastifyBaseLogger } from 'fastify';
2+
import { McpActivityTracker } from './mcp-activity-tracker';
3+
import { deriveClientName, extractSessionId, extractIpAddress } from './client-name-detector';
4+
5+
export interface ActivityTrackingContext {
6+
userId: string;
7+
teamId: string;
8+
authIdentifier: string;
9+
authType: 'oauth' | 'instance_token';
10+
}
11+
12+
/**
13+
* Shared activity tracking logic for MCP requests.
14+
*
15+
* Called from both the OAuth auth middleware (hierarchical router)
16+
* and the instance router (path-based access) to avoid duplication.
17+
* Tracking failure is non-fatal — errors are logged but never propagated.
18+
*/
19+
export function trackMcpActivity(
20+
activityTracker: McpActivityTracker,
21+
request: FastifyRequest,
22+
context: ActivityTrackingContext,
23+
logger: FastifyBaseLogger
24+
): void {
25+
try {
26+
const headers = request.headers as Record<string, string | string[] | undefined>;
27+
const clientName = deriveClientName(headers);
28+
const sessionId = extractSessionId(headers);
29+
const ipAddress = extractIpAddress(headers, request.socket.remoteAddress);
30+
const userAgent = request.headers['user-agent'] || 'unknown';
31+
32+
// Detect tool call from URL path or JSON-RPC body
33+
let isToolCall = false;
34+
if (request.url.includes('tools/call')) {
35+
isToolCall = true;
36+
} else if (request.body && typeof request.body === 'object' && 'method' in request.body) {
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38+
isToolCall = (request.body as any).method === 'tools/call';
39+
}
40+
41+
activityTracker.trackRequest(
42+
context.userId,
43+
context.teamId,
44+
context.authIdentifier,
45+
clientName,
46+
userAgent,
47+
ipAddress,
48+
sessionId,
49+
isToolCall,
50+
context.authType
51+
);
52+
53+
logger.debug({
54+
operation: 'activity_tracked',
55+
userId: context.userId,
56+
teamId: context.teamId,
57+
authIdentifier: context.authIdentifier,
58+
authType: context.authType,
59+
clientName,
60+
isToolCall
61+
}, 'MCP client activity tracked');
62+
} catch (error) {
63+
logger.warn({
64+
operation: 'activity_tracking_failed',
65+
error: error instanceof Error ? error.message : String(error)
66+
}, 'Failed to track MCP client activity (non-fatal)');
67+
}
68+
}

0 commit comments

Comments
 (0)