kibana/x-pack/platform/plugins/shared/onechat/server/plugin.ts
Jedr Blaszyk 8c6fc9e21c
[1chat] MCP Server that can expose 1chat tools (#222231)
## Summary

This PR introduces the **1chat MCP server** in Kibana, exposed at the
experimental `/api/mcp` endpoint behind a feature flag. It allows
external MCP clients (e.g. Claude Desktop, Cursor, OpenAI Agents) to
connect and use tools registered in the 1chat registry.

### MCP server
- Implements a **stateless** MCP server following the MCP spec
(Streamable HTTP transport).
- Supports **API key** and **basic auth** for authentication.
- Works with clients via:
  - **Streamable HTTP** with auth header 
  - **STDIO** transport using `mcp-remote` proxy
- Endpoint under a feature flag `xpack.onechat.mcpServer.enabled`
- 1chat tools are scoped to the caller’s permissions, as determined by
the auth header.

### Other changes
- Implemented `KibanaMcpHttpTransport` (mcp http transport layer adapted
to Kibana Core primitives) + tests

### Local testing

Set ui setting: `onechat:mcpServer:enabled` to true

E.g. add this to Claude Desktop:

```
{
  "mcpServers": {
    "elastic": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://{kbn}/api/mcp",
        "--header",
        "Authorization: ApiKey ${API_KEY}"
      ],
      "env": {
        "API_KEY": "..."
      }
    },
  }
}

```
### Enable feature via API

```
POST kbn:/internal/kibana/settings/onechat:mcpServer:enabled
{"value": true}
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
2025-06-11 15:16:08 +02:00

128 lines
3.7 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger } from '@kbn/logging';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import type { OnechatConfig } from './config';
import type {
OnechatPluginSetup,
OnechatPluginStart,
OnechatSetupDependencies,
OnechatStartDependencies,
} from './types';
import { registerRoutes } from './routes';
import { ServiceManager } from './services';
import { registerFeatures } from './features';
import { ONECHAT_MCP_SERVER_UI_SETTING_ID } from '../common/constants';
export class OnechatPlugin
implements
Plugin<
OnechatPluginSetup,
OnechatPluginStart,
OnechatSetupDependencies,
OnechatStartDependencies
>
{
private logger: Logger;
// @ts-expect-error unused for now
private config: OnechatConfig;
private serviceManager = new ServiceManager();
constructor(context: PluginInitializerContext<OnechatConfig>) {
this.logger = context.logger.get();
this.config = context.config.get();
}
setup(
coreSetup: CoreSetup<OnechatStartDependencies, OnechatPluginStart>,
pluginsSetup: OnechatSetupDependencies
): OnechatPluginSetup {
const serviceSetups = this.serviceManager.setupServices({
logger: this.logger.get('services'),
});
registerFeatures({ features: pluginsSetup.features });
coreSetup.uiSettings.register({
[ONECHAT_MCP_SERVER_UI_SETTING_ID]: {
description: i18n.translate('onechat.uiSettings.mcpServer.description', {
defaultMessage: 'Enables MCP server with access to tools.',
}),
name: i18n.translate('onechat.uiSettings.mcpServer.name', {
defaultMessage: 'MCP Server',
}),
schema: schema.boolean(),
value: false,
readonly: true,
readonlyMode: 'ui',
},
});
const router = coreSetup.http.createRouter();
registerRoutes({
router,
coreSetup,
logger: this.logger,
getInternalServices: () => {
const services = this.serviceManager.internalStart;
if (!services) {
throw new Error('getInternalServices called before service init');
}
return services;
},
});
return {
tools: {
register: serviceSetups.tools.register.bind(serviceSetups.tools),
registerProvider: serviceSetups.tools.registerProvider.bind(serviceSetups.tools),
},
};
}
start(
{ elasticsearch, security }: CoreStart,
{ actions, inference }: OnechatStartDependencies
): OnechatPluginStart {
const startServices = this.serviceManager.startServices({
logger: this.logger.get('services'),
security,
elasticsearch,
actions,
inference,
});
const { tools, agents, runnerFactory } = startServices;
const runner = runnerFactory.getRunner();
return {
tools: {
registry: tools.registry.asPublicRegistry(),
execute: runner.runTool.bind(runner),
asScoped: ({ request }) => {
return {
registry: tools.registry.asScopedPublicRegistry({ request }),
execute: (args) => {
return runner.runTool({ ...args, request });
},
};
},
},
agents: {
registry: agents.registry.asPublicRegistry(),
execute: async (args) => {
return agents.execute(args);
},
},
};
}
stop() {}
}