feat: implementing simple mcp backend to surface actions registry actions
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,28 @@
|
||||
# MCP Backend
|
||||
|
||||
This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend.
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is installed via the `@backstage/plugin-mcp-backend` package. To install it to your backend package, run the following command:
|
||||
|
||||
```bash
|
||||
# From your root directory
|
||||
yarn --cwd packages/backend add @backstage/plugin-mcp-backend
|
||||
```
|
||||
|
||||
Then add the plugin to your backend in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
// ...
|
||||
backend.add(import('@backstage/plugin-mcp-backend'));
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This plugin backend can be started in a standalone mode from directly in this
|
||||
package with `yarn start`. It is a limited setup that is most convenient when
|
||||
developing the plugin backend itself.
|
||||
|
||||
If you want to run the entire project, including the frontend, run `yarn start` from the root directory.
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-mcp-backend
|
||||
title: '@backstage/plugin-mcp-backend'
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { createBackendPlugin } from '@backstage/backend-plugin-api';
|
||||
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(mockServices.auth.factory());
|
||||
backend.add(mockServices.httpAuth.factory());
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Rather than using a real catalog you can use a mock with a fixed set of entities.
|
||||
backend.add(
|
||||
catalogServiceMock.factory({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'sample',
|
||||
title: 'Sample Component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
backend.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'local',
|
||||
register({ registerInit }) {
|
||||
registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'make-greeting',
|
||||
title: 'Test Action',
|
||||
description: 'Test Action',
|
||||
schema: {
|
||||
input: z =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
output: z =>
|
||||
z.object({
|
||||
greeting: z.string(),
|
||||
}),
|
||||
},
|
||||
action: async ({ input }) => ({
|
||||
output: {
|
||||
greeting: `Hello ${input.name}!`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
backend.add(import('../src'));
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@backstage/plugin-mcp-backend",
|
||||
"version": "0.1.0",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "mcp"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { mcpPlugin } from './plugin';
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { json, Router } from 'express';
|
||||
import { McpService } from './services/McpService';
|
||||
import { createStreamableRouter } from './routers/createStreamableRouter';
|
||||
import { createSseRouter } from './routers/createSseRouter';
|
||||
import {
|
||||
actionsRegistryServiceRef,
|
||||
actionsServiceRef,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* mcpPlugin backend plugin
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const mcpPlugin = createBackendPlugin({
|
||||
pluginId: 'mcp',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
actions: actionsServiceRef,
|
||||
registry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actions, logger, httpRouter, httpAuth }) {
|
||||
const mcpService = await McpService.create({
|
||||
actions,
|
||||
});
|
||||
|
||||
const sseRouter = createSseRouter({
|
||||
mcpService,
|
||||
httpAuth,
|
||||
});
|
||||
|
||||
const streamableRouter = createStreamableRouter({
|
||||
mcpService,
|
||||
httpAuth,
|
||||
logger,
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(json());
|
||||
|
||||
router.use('/sse', sseRouter);
|
||||
router.use('/', streamableRouter);
|
||||
|
||||
httpRouter.use(router);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Router from 'express-promise-router';
|
||||
import { McpService } from '../services/McpService';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
* Legacy SSE endpoint for older clients, hopefully will not be needed for much longer.
|
||||
*/
|
||||
export const createSseRouter = ({
|
||||
mcpService,
|
||||
httpAuth,
|
||||
}: {
|
||||
mcpService: McpService;
|
||||
httpAuth: HttpAuthService;
|
||||
}) => {
|
||||
const router = Router();
|
||||
const transportsToSessionId = new Map<string, SSEServerTransport>();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const server = mcpService.getServer({
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
const transport = new SSEServerTransport(
|
||||
`${req.originalUrl}/messages`,
|
||||
res,
|
||||
);
|
||||
|
||||
transportsToSessionId.set(transport.sessionId, transport);
|
||||
|
||||
res.on('close', () => {
|
||||
transportsToSessionId.delete(transport.sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
router.post('/messages', async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transportsToSessionId.get(sessionId);
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
res.status(400).send('No transport found for sessionId');
|
||||
}
|
||||
});
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Router } from 'express';
|
||||
import { McpService } from '../services/McpService';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { isError } from '@backstage/errors';
|
||||
|
||||
export const createStreamableRouter = ({
|
||||
mcpService,
|
||||
httpAuth,
|
||||
logger,
|
||||
}: {
|
||||
mcpService: McpService;
|
||||
logger: LoggerService;
|
||||
httpAuth: HttpAuthService;
|
||||
}): Router => {
|
||||
const router = Router();
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const server = mcpService.getServer({
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
// stateless implementation for now, so that we can support multiple
|
||||
// instances of the server backend, and avoid sticky sessions.
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
server.close();
|
||||
});
|
||||
} catch (error) {
|
||||
if (isError(error)) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error',
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async (_, res) => {
|
||||
// We only support POST requests, so we return a 405 error for all other methods.
|
||||
res.writeHead(405).end(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Method not allowed.',
|
||||
},
|
||||
id: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
router.delete('/', async (_, res) => {
|
||||
// We only support POST requests, so we return a 405 error for all other methods.
|
||||
res.writeHead(405).end(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Method not allowed.',
|
||||
},
|
||||
id: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mockCredentials } from '@backstage/backend-test-utils';
|
||||
import { McpService } from './McpService';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
ListToolsResultSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
describe('McpService', () => {
|
||||
it('should list the available actions as tools in the mcp backend', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockActionsRegistry.register({
|
||||
name: 'mock-action',
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
schema: {
|
||||
input: z => z.object({ input: z.string() }),
|
||||
output: z => z.object({ output: z.string() }),
|
||||
},
|
||||
action: async () => ({ output: { output: 'test' } }),
|
||||
});
|
||||
|
||||
const mcpService = await McpService.create({
|
||||
actions: mockActionsRegistry,
|
||||
});
|
||||
|
||||
const server = mcpService.getServer({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: 'test client',
|
||||
version: '1.0',
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const result = await client.request(
|
||||
{
|
||||
method: 'tools/list',
|
||||
},
|
||||
ListToolsResultSchema,
|
||||
);
|
||||
|
||||
expect(result.tools).toEqual([
|
||||
{
|
||||
annotations: {
|
||||
destructiveHint: true,
|
||||
idempotentHint: false,
|
||||
openWorldHint: false,
|
||||
readOnlyHint: false,
|
||||
title: 'Test',
|
||||
},
|
||||
description: 'Test',
|
||||
inputSchema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
input: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['input'],
|
||||
type: 'object',
|
||||
},
|
||||
name: 'mock-action',
|
||||
outputSchema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
output: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['output'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call the action when the tool is invoked', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockAction = jest.fn(async () => ({ output: { output: 'test' } }));
|
||||
|
||||
mockActionsRegistry.register({
|
||||
name: 'mock-action',
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
schema: {
|
||||
input: z => z.object({ input: z.string() }),
|
||||
output: z => z.object({ output: z.string() }),
|
||||
},
|
||||
action: mockAction,
|
||||
});
|
||||
|
||||
const mcpService = await McpService.create({
|
||||
actions: mockActionsRegistry,
|
||||
});
|
||||
|
||||
const server = mcpService.getServer({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: 'test client',
|
||||
version: '1.0',
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const result = await client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: { name: 'mock-action', arguments: { input: 'test' } },
|
||||
},
|
||||
CallToolResultSchema,
|
||||
);
|
||||
|
||||
expect(mockAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
credentials: mockCredentials.user(),
|
||||
input: { input: 'test' },
|
||||
logger: expect.anything(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.structuredContent).toEqual({ output: 'test' });
|
||||
});
|
||||
|
||||
it('should return an error when the action is not found', async () => {
|
||||
const mcpService = await McpService.create({
|
||||
actions: actionsRegistryServiceMock(),
|
||||
});
|
||||
|
||||
const server = mcpService.getServer({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: 'test client',
|
||||
version: '1.0',
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
await expect(
|
||||
client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: { name: 'mock-action', arguments: { input: 'test' } },
|
||||
},
|
||||
CallToolResultSchema,
|
||||
),
|
||||
).rejects.toThrow('Action mock-action not found');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { version } from '@backstage/plugin-mcp-backend/package.json';
|
||||
|
||||
export class McpService {
|
||||
constructor(private readonly actions: ActionsService) {}
|
||||
|
||||
static async create({ actions }: { actions: ActionsService }) {
|
||||
return new McpService(actions);
|
||||
}
|
||||
|
||||
getServer({ credentials }: { credentials: BackstageCredentials }) {
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: 'backstage',
|
||||
// TODO: this version will most likely change in the future.
|
||||
version,
|
||||
},
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const { actions } = await this.actions.list({ credentials });
|
||||
|
||||
return {
|
||||
tools: actions.map(action => ({
|
||||
inputSchema: action.schema.input,
|
||||
outputSchema: action.schema.output,
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
annotations: {
|
||||
title: action.title,
|
||||
destructiveHint: action.attributes.destructive,
|
||||
idempotentHint: action.attributes.idempotent,
|
||||
readOnlyHint: action.attributes.readOnly,
|
||||
openWorldHint: false,
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
||||
const { actions } = await this.actions.list({ credentials });
|
||||
const action = actions.find(a => a.name === params.name);
|
||||
|
||||
if (!action) {
|
||||
throw new Error(`Action ${params.name} not found`);
|
||||
}
|
||||
|
||||
const { output } = await this.actions.invoke({
|
||||
id: action.id,
|
||||
input: params.arguments as JsonObject,
|
||||
credentials,
|
||||
});
|
||||
|
||||
return {
|
||||
// all actions need to be defined with an structured output,
|
||||
// so we can return the output using this format rather than
|
||||
// just text.
|
||||
structuredContent: output,
|
||||
};
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user