Skip to content

Commit 84b6774

Browse files
authored
Use a Service token for the mcp tools (#1693)
Part of OPS-3127.
1 parent 9b6afa3 commit 84b6774

File tree

8 files changed

+57
-8
lines changed

8 files changed

+57
-8
lines changed

packages/server/api/src/app/ai/mcp/openops-tools.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import fs from 'fs/promises';
1111
import { OpenAPI } from 'openapi-types';
1212
import os from 'os';
1313
import path from 'path';
14+
import { accessTokenManager } from '../../authentication/context/access-token-manager';
1415
import { MCPTool } from './types';
1516

1617
const INCLUDED_PATHS: Record<string, string[]> = {
@@ -68,7 +69,7 @@ async function getOpenApiSchemaPath(app: FastifyInstance): Promise<string> {
6869

6970
export async function getOpenOpsTools(
7071
app: FastifyInstance,
71-
authToken: string,
72+
userAuthToken: string,
7273
): Promise<MCPTool> {
7374
const basePath = system.getOrThrow<string>(
7475
AppSystemProp.OPENOPS_MCP_SERVER_PATH,
@@ -79,13 +80,17 @@ export async function getOpenOpsTools(
7980

8081
const tempSchemaPath = await getOpenApiSchemaPath(app);
8182

83+
const serviceToken = await accessTokenManager.generateServiceToken(
84+
userAuthToken,
85+
);
86+
8287
const openopsClient = await experimental_createMCPClient({
8388
transport: new Experimental_StdioMCPTransport({
8489
command: pythonPath,
8590
args: [serverPath],
8691
env: {
8792
OPENAPI_SCHEMA_PATH: tempSchemaPath,
88-
AUTH_TOKEN: authToken,
93+
AUTH_TOKEN: serviceToken,
8994
API_BASE_URL: networkUtls.getInternalApiUrl(),
9095
OPENOPS_MCP_SERVER_PATH: basePath,
9196
LOGZIO_TOKEN: system.get<string>(SharedSystemProp.LOGZIO_TOKEN) ?? '',

packages/server/api/src/app/app-connection/app-connection.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ const UpsertAppConnectionRequest = {
171171

172172
const PatchAppConnectionRequest = {
173173
config: {
174-
allowedPrincipals: [PrincipalType.USER],
174+
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
175175
permission: Permission.WRITE_APP_CONNECTION,
176176
},
177177
schema: {
@@ -226,7 +226,7 @@ const DeleteAppConnectionRequest = {
226226

227227
const GetAppConnectionRequest = {
228228
config: {
229-
allowedPrincipals: [PrincipalType.USER],
229+
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
230230
permission: Permission.READ_APP_CONNECTION,
231231
},
232232
schema: {
@@ -253,7 +253,7 @@ const GetAppConnectionRequest = {
253253

254254
const GetConnectionMetadataRequest = {
255255
config: {
256-
allowedPrincipals: [PrincipalType.USER],
256+
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
257257
permission: Permission.READ_APP_CONNECTION,
258258
},
259259
schema: {

packages/server/api/src/app/authentication/context/access-token-manager.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,32 @@ export const accessTokenManager = {
8181
});
8282
},
8383

84+
async generateServiceToken(
85+
userToken: string,
86+
expiresInSeconds: number = openOpsRefreshTokenLifetimeSeconds,
87+
): Promise<string> {
88+
const principal = await this.extractPrincipal(userToken);
89+
if (principal.type !== PrincipalType.USER) {
90+
throw new ApplicationError({
91+
code: ErrorCode.INVALID_BEARER_TOKEN,
92+
params: {
93+
message: 'Service token can only be generated from a user token',
94+
},
95+
});
96+
}
97+
98+
const secret = await jwtUtils.getJwtSecret();
99+
100+
return jwtUtils.sign({
101+
payload: {
102+
...principal,
103+
type: PrincipalType.SERVICE,
104+
},
105+
key: secret,
106+
expiresInSeconds,
107+
});
108+
},
109+
84110
async extractPrincipal(token: string): Promise<Principal> {
85111
const secret = await jwtUtils.getJwtSecret();
86112

packages/server/api/src/app/blocks/base-block-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ const ListCategoriesRequest = {
230230
};
231231

232232
const OptionsBlockRequest = {
233+
config: {
234+
allowedPrincipals: ALL_PRINCIPAL_TYPES,
235+
},
233236
schema: {
234237
operationId: 'Execute Block Properties',
235238
description:

packages/server/api/src/app/file/file.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const fileController: FastifyPluginAsyncTypebox = async (app) => {
1919

2020
const GetFileRequest = {
2121
config: {
22-
allowedPrincipals: [PrincipalType.USER],
22+
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
2323
},
2424
schema: {
2525
operationId: 'Get File',

packages/server/api/src/app/flows/flow-run/flow-run-controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ const ResumeFlowRunRequest = {
190190

191191
const RetryFlowRequest = {
192192
config: {
193+
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
193194
permission: Permission.RETRY_RUN,
194195
},
195196
schema: {

packages/server/api/src/app/flows/flow/flow.controller.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,9 @@ const ListFlowsRequestOptions = {
381381
};
382382

383383
const CountFlowsRequestOptions = {
384+
config: {
385+
allowedPrincipals: [PrincipalType.SERVICE, PrincipalType.USER],
386+
},
384387
schema: {
385388
operationId: 'Get Flow Count',
386389
description:

packages/server/api/test/unit/ai/openops-tools.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const mockTools = {
1616
const systemMock = {
1717
get: jest.fn(),
1818
getOrThrow: jest.fn(),
19+
getNumber: jest.fn(),
1920
};
2021

2122
const loggerMock = {
@@ -27,8 +28,17 @@ const networkUtlsMock = {
2728
getInternalApiUrl: jest.fn(),
2829
};
2930

30-
const createMcpClientMock = jest.fn();
31+
const generateServiceTokenMock = jest.fn();
32+
jest.mock(
33+
'../../../src/app/authentication/context/access-token-manager',
34+
() => ({
35+
accessTokenManager: {
36+
generateServiceToken: generateServiceTokenMock,
37+
},
38+
}),
39+
);
3140

41+
const createMcpClientMock = jest.fn();
3242
jest.mock('ai', () => ({
3343
experimental_createMCPClient: createMcpClientMock,
3444
}));
@@ -214,6 +224,7 @@ describe('getOpenOpsTools', () => {
214224
tools: jest.fn().mockResolvedValue(mockTools),
215225
};
216226
createMcpClientMock.mockResolvedValue(mockClient);
227+
generateServiceTokenMock.mockResolvedValue('auth-service-token');
217228

218229
const result = await getOpenOpsTools(mockApp, 'test-auth-token');
219230

@@ -229,7 +240,7 @@ describe('getOpenOpsTools', () => {
229240
args: [`${mockBasePath}/main.py`],
230241
env: expect.objectContaining({
231242
OPENAPI_SCHEMA_PATH: expect.any(String),
232-
AUTH_TOKEN: 'test-auth-token',
243+
AUTH_TOKEN: 'auth-service-token',
233244
API_BASE_URL: mockApiBaseUrl,
234245
OPENOPS_MCP_SERVER_PATH: mockBasePath,
235246
LOGZIO_TOKEN: 'test-logzio-token',

0 commit comments

Comments
 (0)