Skip to content

Commit f4966ed

Browse files
authored
feat: Initial MCP server for GenAI plugin (#397)
1 parent 977ba92 commit f4966ed

File tree

9 files changed

+655
-20
lines changed

9 files changed

+655
-20
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ repos:
2626
- id: lint
2727
language: node
2828
name: Run linting
29-
entry: yarn lint-staged
29+
entry: yarn lint-staged --no-stash
3030
stage: [commit]

plugins/genai/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@backstage/config": "^1.2.0",
4646
"@backstage/plugin-catalog-node": "^1.14.0",
4747
"@langchain/core": "0.3.57",
48+
"@modelcontextprotocol/sdk": "^1.12.3",
4849
"@types/express": "*",
4950
"express": "^4.17.1",
5051
"express-promise-router": "^4.1.0",

plugins/genai/backend/src/plugin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
createBackstageTechDocsSearchTool,
3333
} from './tools';
3434
import { DatabaseSessionStore } from './database';
35+
import { McpService } from './service/McpService';
3536

3637
export const awsGenAiPlugin = createBackendPlugin({
3738
pluginId: 'aws-genai',
@@ -94,10 +95,13 @@ export const awsGenAiPlugin = createBackendPlugin({
9495
sessionStore,
9596
});
9697

98+
const mcpService = await McpService.fromConfig(agentService);
99+
97100
httpRouter.use(
98101
await createRouter({
99102
logger: winstonLogger,
100103
agentService,
104+
mcpService,
101105
httpAuth,
102106
discovery,
103107
auth,

plugins/genai/backend/src/service/DefaultAgentService.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ChatEvent,
3232
ChatSession,
3333
GenerateResponse,
34+
SyncResponse,
3435
} from '@aws/genai-plugin-for-backstage-common';
3536
import { SessionStore } from '../database';
3637

@@ -111,6 +112,10 @@ export class DefaultAgentService implements AgentService {
111112
return service;
112113
}
113114

115+
getAgents(): Agent[] {
116+
return Array.from(this.agents.values());
117+
}
118+
114119
async stream(
115120
userMessage: string,
116121
options: {
@@ -133,7 +138,7 @@ export class DefaultAgentService implements AgentService {
133138
const agent = this.getActualAgent(options.agentName);
134139

135140
if (!sessionId) {
136-
session = await this.createSession(agentName, principal, false);
141+
session = await this.makeSession(agentName, principal, false);
137142

138143
newSession = true;
139144
} else {
@@ -172,6 +177,19 @@ export class DefaultAgentService implements AgentService {
172177
return this.sessionStore.getSession(agentName, sessionId, principal);
173178
}
174179

180+
async createSession(options: {
181+
agentName: string;
182+
credentials: BackstageCredentials<
183+
BackstageUserPrincipal | BackstageServicePrincipal
184+
>;
185+
}): Promise<ChatSession> {
186+
const { agentName, credentials } = options;
187+
188+
const { principal } = await this.getUserEntityRef(credentials);
189+
190+
return this.makeSession(agentName, principal, false);
191+
}
192+
175193
async endSession(options: {
176194
agentName: string;
177195
sessionId: string;
@@ -186,6 +204,35 @@ export class DefaultAgentService implements AgentService {
186204
await this.sessionStore.endSession(agentName, sessionId, principal);
187205
}
188206

207+
async sync(
208+
userMessage: string,
209+
options: {
210+
agentName: string;
211+
sessionId?: string;
212+
credentials: BackstageCredentials<
213+
BackstageUserPrincipal | BackstageServicePrincipal
214+
>;
215+
},
216+
): Promise<SyncResponse> {
217+
const { principal, userEntityRef } = await this.getUserEntityRef(
218+
options.credentials,
219+
);
220+
221+
const agent = this.getActualAgent(options.agentName);
222+
223+
const session = await this.makeSession(options.agentName, principal, true);
224+
225+
const output = agent.generate(userMessage, session.sessionId, {
226+
userEntityRef,
227+
credentials: options.credentials,
228+
});
229+
230+
return {
231+
output,
232+
sessionId: session.sessionId,
233+
};
234+
}
235+
189236
async generate(
190237
prompt: string,
191238
options: {
@@ -201,11 +248,7 @@ export class DefaultAgentService implements AgentService {
201248

202249
const agent = this.getActualAgent(options.agentName);
203250

204-
const session = await this.createSession(
205-
options.agentName,
206-
principal,
207-
true,
208-
);
251+
const session = await this.makeSession(options.agentName, principal, true);
209252

210253
return agent.generate(prompt, session.sessionId, {
211254
userEntityRef,
@@ -227,7 +270,7 @@ export class DefaultAgentService implements AgentService {
227270
return this.agents.get(agent);
228271
}
229272

230-
private createSession(agent: string, principal: string, ended: boolean) {
273+
private makeSession(agent: string, principal: string, ended: boolean) {
231274
const sessionId = uuidv4();
232275

233276
this.logger.info(`Generated session ${sessionId}`);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* Licensed under the Apache License, Version 2.0 (the "License").
4+
* You may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
import { BackstageCredentials } from '@backstage/backend-plugin-api';
15+
import { AgentService } from './types';
16+
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
17+
import {
18+
ListToolsRequestSchema,
19+
CallToolRequestSchema,
20+
} from '@modelcontextprotocol/sdk/types.js';
21+
import { version } from '@aws/genai-plugin-for-backstage-backend/package.json';
22+
23+
export class McpService {
24+
public constructor(private readonly agentService: AgentService) {}
25+
26+
static async fromConfig(agentService: AgentService) {
27+
return new McpService(agentService);
28+
}
29+
30+
getServer(options: { agentName: string; credentials: BackstageCredentials }) {
31+
const { agentName, credentials } = options;
32+
33+
const agent = this.agentService.getAgent(agentName);
34+
35+
if (!agent) {
36+
throw new Error(`Agent ${agentName} not found`);
37+
}
38+
39+
const server = new McpServer(
40+
{
41+
name: `backstage-${agentName}`,
42+
version,
43+
},
44+
{ capabilities: { tools: {} } },
45+
);
46+
47+
server.setRequestHandler(ListToolsRequestSchema, async () => {
48+
return {
49+
tools: [
50+
{
51+
inputSchema: {
52+
type: 'object',
53+
properties: {
54+
query: {
55+
type: 'string',
56+
description: 'A natural language question for the agent.',
57+
},
58+
},
59+
required: ['query'],
60+
},
61+
62+
name: agent.getName(),
63+
description: agent.getDescription(),
64+
annotations: {
65+
destructiveHint: true,
66+
idempotentHint: false,
67+
readOnlyHint: false,
68+
openWorldHint: true,
69+
},
70+
},
71+
],
72+
};
73+
});
74+
75+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
76+
if (params.arguments?.query) {
77+
const result = await this.agentService.generate(
78+
params.arguments.query as string,
79+
{
80+
agentName: params.name,
81+
credentials,
82+
},
83+
);
84+
85+
return {
86+
content: [
87+
{
88+
type: 'text',
89+
text: result.output,
90+
},
91+
],
92+
};
93+
}
94+
95+
throw new Error('No query provided');
96+
});
97+
98+
return server;
99+
}
100+
}

plugins/genai/backend/src/service/router.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ import {
3030
ChatEvent,
3131
EndSessionRequest,
3232
} from '@aws/genai-plugin-for-backstage-common';
33+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
34+
import { McpService } from './McpService';
3335

3436
export interface RouterOptions {
3537
logger: LoggerService;
3638
agentService: AgentService;
39+
mcpService: McpService;
3740
discovery: DiscoveryService;
3841
auth?: AuthService;
3942
httpAuth?: HttpAuthService;
@@ -42,7 +45,7 @@ export interface RouterOptions {
4245
export async function createRouter(
4346
options: RouterOptions,
4447
): Promise<express.Router> {
45-
const { logger, agentService } = options;
48+
const { logger, agentService, mcpService } = options;
4649

4750
const router = Router();
4851
router.use(express.json());
@@ -139,6 +142,41 @@ export async function createRouter(
139142
response.status(200).send();
140143
});
141144

145+
router.post('/v1/mcp/:agent', async (request, response) => {
146+
const { agent } = request.params;
147+
148+
const credentials = await httpAuth.credentials(request);
149+
150+
try {
151+
const server = mcpService.getServer({
152+
agentName: agent,
153+
credentials,
154+
});
155+
const transport: StreamableHTTPServerTransport =
156+
new StreamableHTTPServerTransport({
157+
sessionIdGenerator: undefined,
158+
});
159+
response.on('close', () => {
160+
transport.close();
161+
server.close();
162+
});
163+
await server.connect(transport);
164+
await transport.handleRequest(request, response, request.body);
165+
} catch (error) {
166+
logger.error('Error handling MCP request:', error as Error);
167+
if (!response.headersSent) {
168+
response.status(500).json({
169+
jsonrpc: '2.0',
170+
error: {
171+
code: -32603,
172+
message: 'Internal server error',
173+
},
174+
id: null,
175+
});
176+
}
177+
}
178+
});
179+
142180
router.get('/health', (_, response) => {
143181
logger.info('PONG!');
144182
response.json({ status: 'ok' });

plugins/genai/backend/src/service/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ import {
1515
ChatEvent,
1616
ChatSession,
1717
GenerateResponse,
18+
SyncResponse,
1819
} from '@aws/genai-plugin-for-backstage-common';
1920
import { BackstageCredentials } from '@backstage/backend-plugin-api';
21+
import { Agent } from '../agent/Agent';
2022

2123
export interface AgentService {
24+
getAgents(): Agent[];
25+
26+
getAgent(agentName: string): Agent | undefined;
27+
2228
stream(
2329
userMessage: string,
2430
options: {
@@ -28,6 +34,15 @@ export interface AgentService {
2834
},
2935
): Promise<ReadableStream<ChatEvent>>;
3036

37+
sync(
38+
userMessage: string,
39+
options: {
40+
agentName: string;
41+
sessionId?: string;
42+
credentials: BackstageCredentials;
43+
},
44+
): Promise<SyncResponse>;
45+
3146
generate(
3247
prompt: string,
3348
options: {
@@ -36,6 +51,11 @@ export interface AgentService {
3651
},
3752
): Promise<GenerateResponse>;
3853

54+
createSession(options: {
55+
agentName: string;
56+
credentials: BackstageCredentials;
57+
}): Promise<ChatSession>;
58+
3959
endSession(options: {
4060
agentName: string;
4161
sessionId: string;

plugins/genai/common/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export interface AgentRequestOptions {
2626
token: string;
2727
}
2828

29+
export interface SyncResponse {
30+
sessionId: string;
31+
output: any;
32+
}
33+
2934
export interface GenerateResponse {
3035
output: any;
3136
}

0 commit comments

Comments
 (0)