kibana/x-pack/platform/plugins/shared/onechat/README.md
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

5.9 KiB

Onechat plugin

Home of the workchat framework.

Note: as many other platform features, onechat isolates its public types and static utils, exposed from packages, from its APIs, exposed from the plugin.

The onechat plugin has 3 main packages:

  • @kbn/onechat-common: types and utilities which are shared between browser and server
  • @kbn/onechat-server: server-specific types and utilities
  • @kbn/onechat-browser: browser-specific types and utilities.

Overview

The onechat plugin exposes APIs to interact with onechat primitives.

The main primitives are:

Additionally, the plugin implements MCP server that exposes onechat tools.

Tools

A tool can be thought of as a LLM-friendly function, with the metadata required for the LLM to understand its purpose and how to call it attached to it.

Tool can come from multiple sources: built-in from Kibana, from MCP servers, and so on. At the moment, only built-in tools are implemented

Registering a built-in tool

Basic example

class MyPlugin {
  setup(core: CoreSetup, { onechat }: { onechat: OnechatPluginSetup }) {
    onechat.tools.register({
      id: 'my_tool',
      name: 'My Tool',
      description: 'My very first tool',
      meta: {
        tags: ['foo', 'bar'],
      },
      schema: z.object({
        someNumber: z.number().describe('Some random number'),
      }),
      handler: ({ someNumber }, context) => {
        return 42 + someNumber;
      },
    });
  }
}

using the handler context to use scoped services

onechat.tools.register({
  id: 'my_es_tool',
  name: 'My Tool',
  description: 'Some example',
  schema: z.object({
    indexPattern: z.string().describe('Index pattern to filter on'),
  }),
  handler: async ({ indexPattern }, { modelProvider, esClient }) => {
    const indices = await esClient.asCurrentUser.cat.indices({ index: indexPattern });

    const model = await modelProvider.getDefaultModel();
    const response = await model.inferenceClient.chatComplete(somethingWith(indices));

    return response;
  },
});

emitting events

onechat.tools.register({
  id: 'my_es_tool',
  name: 'My Tool',
  description: 'Some example',
  schema: z.object({}),
  handler: async ({}, { events }) => {
    events.emit({
      type: 'my_custom_event',
      data: { stage: 'before' },
    });

    const response = doSomething();

    events.emit({
      type: 'my_custom_event',
      data: { stage: 'after' },
    });

    return response;
  },
});

Executing a tool

Executing a tool can be done using the execute API of the onechat tool start service:

const { result } = await onechat.tools.execute({
  toolId: 'my_tool',
  toolParams: { someNumber: 9000 },
  request,
});

It can also be done directly from a tool definition:

const tool = await onechat.tools.registry.get({ toolId: 'my_tool', request });
const { result } = await tool.execute({ toolParams: { someNumber: 9000 } });

Event handling

Tool execution emits toolCall and toolResponse events:

import { isToolCallEvent, isToolResponseEvent } from '@kbn/onechat-server';

const { result } = await onechat.tools.execute({
  toolId: 'my_tool',
  toolParams: { someNumber: 9000 },
  request,
  onEvent: (event) => {
    if (isToolCallEvent(event)) {
      const {
        data: { toolId, toolParams },
      } = event;
    }
    if (isToolResponseEvent(event)) {
      const {
        data: { toolResult },
      } = event;
    }
  },
});

Tool identifiers

Because tools are coming from multiple sources, and because we need to be able to identify which source a given tool is coming from (e.g. for execution), we're using the concept of tool identifier to represent more than a plain id.

Tool identifier come into 3 shapes:

  • PlainIdToolIdentifier: plain tool identifiers
  • StructuredToolIdentifier: structured (object version)
  • SerializedToolIdentifier: serialized string version

Using a plain id is always possible but discouraged, as in case of id conflict, the system will then just pick an arbitrary tool in any source available.

E.g. avoid doing:

await onechat.tools.execute({
  toolId: 'my_tool',
  toolParams: { someNumber: 9000 },
  request,
});

And instead do:

import { ToolSourceType, builtinSourceId } from '@kbn/onechat-common';

await onechat.tools.execute({
  toolId: {
    toolId: 'my_tool',
    sourceType: ToolSourceType.builtIn,
    sourceId: builtinSourceId,
  },
  toolParams: { someNumber: 9000 },
  request,
});

Or, with the corresponding utility:

import { createBuiltinToolId } from '@kbn/onechat-common';

await onechat.tools.execute({
  toolId: createBuiltinToolId('my_tool'),
  toolParams: { someNumber: 9000 },
  request,
});

Error handling

All onechat errors inherit from the OnechatError error type. Various error utilities are exposed from the @kbn/onechat-common package to identify and handle those errors.

Some simple example of handling a specific type of error:

import { isToolNotFoundError } from '@kbn/onechat-common';

try {
  const { result } = await onechat.tools.execute({
    toolId: 'my_tool',
    toolParams: { someNumber: 9000 },
    request,
  });
} catch (e) {
  if (isToolNotFoundError(e)) {
    throw new Error(`run ${e.meta.runId} failed because tool was not found`);
  }
}

MCP Server

The MCP server provides a standardized interface for external MCP clients to access onechat tools.

Running with Claude Desktop

To enable the MCP server, add the following to your Kibana config:

uiSettings.overrides:
  onechat:mcpServer:enabled: true

Configure Claude Desktop by adding this to its configuration:

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