Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The Model Context Protocol (MCP) is an open protocol that enables seamless integ

## Features

- **151 Tools** across 33 categories for comprehensive Countly operations
- **155 Tools** across 34 categories for comprehensive Countly operations
- **Resources** for AI context - Access read-only Countly data (app configs, event schemas, analytics overviews)
- **Prompts** for common tasks - Pre-built templates for crash analysis, engagement reports, and more
- **Multiple Transport Options**: Supports both stdio (recommended) and HTTP/SSE connections
Expand All @@ -42,7 +42,7 @@ The Model Context Protocol (MCP) is an open protocol that enables seamless integ

This server implements the full MCP specification with support for:

### Tools (151 available)
### Tools (155 available)
Execute Countly operations like analytics queries, app management, crash analysis, etc.

### Resources
Expand Down Expand Up @@ -282,7 +282,7 @@ COUNTLY_TOOLS_ALERTS=NONE # Alerts: Completely disabled
COUNTLY_TOOLS_ALL=R # Read-only mode for all tools
```

**Available Categories** (subset — see TOOLS_CONFIGURATION.md for all 33):
**Available Categories** (subset — see TOOLS_CONFIGURATION.md for all 34):
- `CORE` - Core tools (ping, get_version, get_plugins) (3 tools)
- `APPS` - Application management (6 tools)
- `ANALYTICS` - Analytics data retrieval (7 tools)
Expand Down Expand Up @@ -555,7 +555,7 @@ For HTTP mode, clients should connect to: `http://your-server:3000/mcp`

## Available Tools

The server provides 151 tools across 33 categories for comprehensive Countly integration:
The server provides 155 tools across 34 categories for comprehensive Countly integration:

### Core Tools (OpenAI/ChatGPT Compatible)
- **`ping`** - Check if Countly server is healthy and reachable
Expand Down Expand Up @@ -767,6 +767,12 @@ The server provides 151 tools across 33 categories for comprehensive Countly int
- **`content_assets_delete`** - Delete an uploaded content asset.
- **`content_langs_list`** - List languages eligible for content translations.

### Knowledge Base (requires `knowledge-base` plugin)
- **`knowledge_base_spaces`** - List knowledge base spaces the user can read (discover space ids first).
- **`knowledge_base_search`** - Semantic search over the knowledge base; returns ranked doc sections with deep-link citations, permission-scoped to the token.
- **`knowledge_base_ask`** - Ask a question; the server retrieves matching docs and returns a cited answer (LLM-synthesized when configured, extractive otherwise).
- **`knowledge_base_write`** - Write a page from Markdown; pass a stable `external_ref` for idempotent upserts (ideal for AI agents logging decisions).

All tools support flexible app identification via either `app_id` or `app_name` parameter.

## Health Check
Expand Down
35 changes: 35 additions & 0 deletions TOOLS_CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The following categories are **only available if their corresponding plugin is i
- **funnels** → requires `funnels` plugin
- **journeys** → requires `journey_engine` plugin (Countly Enterprise)
- **content** → requires `content` plugin (Countly Enterprise)
- **knowledge_base** → requires `knowledge-base` plugin

### Categories Available by Default

Expand Down Expand Up @@ -644,6 +645,40 @@ async function contentExamples() {
}
```

### knowledge_base
**Tools**: `knowledge_base_spaces`, `knowledge_base_search`, `knowledge_base_ask`, `knowledge_base_write`

**Requires plugin**: `knowledge-base`

Search and write the server's built-in knowledge base (documentation spaces). Reads are permission-scoped to the spaces the authenticated user can see; writes require create rights on the target space and accept Markdown.

**Examples:**
```typescript
async function knowledgeBaseExamples() {
// Discover space ids first
const spaces = await tools.knowledge_base_spaces({});

// Semantic search with deep-link citations
const hits = await tools.knowledge_base_search({
query: 'how do we handle ingestion retries?',
limit: 5
});

// Cited answer (LLM-synthesized when the server has an LLM configured)
const answer = await tools.knowledge_base_ask({
query: 'what was decided about duplicate events?'
});

// Record a decision; same external_ref updates the same page next time
await tools.knowledge_base_write({
space_id: '507f1f77bcf86cd799439011',
title: 'Feature X decisions',
markdown: '# Feature X\n\n- Chose approach A because ...',
external_ref: 'feature-x'
});
}
```

## Verification

The server will log the active configuration on startup:
Expand Down
10 changes: 10 additions & 0 deletions src/lib/tools-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,16 @@ export const TOOL_CATEGORIES: Record<string, ToolCategoryConfig> = {
requiresPlugin: 'content',
availableByDefault: false,
},
knowledge_base: {
operations: {
'knowledge_base_spaces': 'R',
'knowledge_base_search': 'R',
'knowledge_base_ask': 'R',
'knowledge_base_write': 'C',
},
requiresPlugin: 'knowledge-base',
availableByDefault: false,
},
};

/**
Expand Down
8 changes: 8 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ import { contentToolDefinitions, contentToolHandlers, contentToolMetadata, Conte

export { contentToolDefinitions, contentToolHandlers, contentToolMetadata, ContentTools };

// Knowledge Base
import { knowledgeBaseToolDefinitions, knowledgeBaseToolHandlers, knowledgeBaseToolMetadata, KnowledgeBaseTools } from './knowledge-base.js';

export { knowledgeBaseToolDefinitions, knowledgeBaseToolHandlers, knowledgeBaseToolMetadata, KnowledgeBaseTools };

// Type definitions
export type { ToolContext, ToolResult } from './types.js';

Expand Down Expand Up @@ -204,6 +209,7 @@ export function getAllToolDefinitions() {
...hooksToolDefinitions,
...journeysToolDefinitions,
...contentToolDefinitions,
...knowledgeBaseToolDefinitions,
];
}

Expand Down Expand Up @@ -245,6 +251,7 @@ export function getAllToolHandlers() {
...hooksToolHandlers,
...journeysToolHandlers,
...contentToolHandlers,
...knowledgeBaseToolHandlers,
};
}

Expand Down Expand Up @@ -286,5 +293,6 @@ export function getAllToolMetadata() {
hooksToolMetadata,
journeysToolMetadata,
contentToolMetadata,
knowledgeBaseToolMetadata,
];
}
234 changes: 234 additions & 0 deletions src/tools/knowledge-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { ToolContext, ToolResult } from './types.js';
import { safeApiCall } from '../lib/error-handler.js';

// ============================================================================
// KNOWLEDGE_BASE_SPACES TOOL
// ============================================================================

export const listKnowledgeBaseSpacesToolDefinition = {
name: 'knowledge_base_spaces',
description: 'List knowledge base spaces the current user can read via /o/kb/spaces. Call this first to discover space ids for knowledge_base_search, knowledge_base_ask and knowledge_base_write. Requires the knowledge-base plugin.',
inputSchema: {
type: 'object',
properties: {},
},
};

export async function handleListKnowledgeBaseSpaces(context: ToolContext, _args: any): Promise<ToolResult> {
const params = {
...context.getAuthParams(),
};

const response = await safeApiCall(
() => context.httpClient.get('/o/kb/spaces', { params }),
'Failed to execute request to /o/kb/spaces'
);

const spaces = Array.isArray(response.data) ? response.data : [];

return {
content: [
{
type: 'text',
text: `Found ${spaces.length} knowledge base space(s):\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
}

// ============================================================================
// KNOWLEDGE_BASE_SEARCH TOOL
// ============================================================================

export const searchKnowledgeBaseToolDefinition = {
name: 'knowledge_base_search',
description: 'Semantic search over the Countly knowledge base via /o/kb/rag-search. Returns ranked documentation sections with deep-link citations (title, heading, url, snippet, score). Results are limited to spaces the authenticated user may read. Requires the knowledge-base plugin with semantic search enabled; use knowledge_base_ask instead for a synthesized answer.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Natural-language query describing what to look for.' },
limit: { type: 'number', description: 'Maximum number of results (default 10, capped at 50 by the server).' },
space_id: { type: 'string', description: 'Optional: restrict the search to one space. Call knowledge_base_spaces to discover ids.' },
},
required: ['query'],
},
};

export async function handleSearchKnowledgeBase(context: ToolContext, args: any): Promise<ToolResult> {
const { query, limit, space_id } = args;

const params: any = {
...context.getAuthParams(),
query,
};
if (limit !== undefined) {
params.limit = limit;
}
if (space_id) {
params.space_id = space_id;
}

const response = await safeApiCall(
() => context.httpClient.get('/o/kb/rag-search', { params }),
'Failed to execute request to /o/kb/rag-search'
);

const results = response.data?.results || [];

return {
content: [
{
type: 'text',
text: `Found ${results.length} knowledge base result(s) for "${query}":\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
}

// ============================================================================
// KNOWLEDGE_BASE_ASK TOOL
// ============================================================================

export const askKnowledgeBaseToolDefinition = {
name: 'knowledge_base_ask',
description: 'Ask the Countly knowledge base a question via /o/kb/ask. The server retrieves matching documentation (permission-scoped) and returns an answer with cited sources: an LLM-synthesized answer when the server has an LLM configured ("llm" mode), otherwise the top passages verbatim ("extractive" mode). Requires the knowledge-base plugin with semantic search enabled; use knowledge_base_search for raw ranked results.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'The question to answer from the knowledge base.' },
limit: { type: 'number', description: 'Maximum number of source passages to retrieve (default 10, capped at 50 by the server).' },
space_id: { type: 'string', description: 'Optional: restrict retrieval to one space. Call knowledge_base_spaces to discover ids.' },
},
required: ['query'],
},
};

export async function handleAskKnowledgeBase(context: ToolContext, args: any): Promise<ToolResult> {
const { query, limit, space_id } = args;

const params: any = {
...context.getAuthParams(),
query,
};
if (limit !== undefined) {
params.limit = limit;
}
if (space_id) {
params.space_id = space_id;
}

const response = await safeApiCall(
() => context.httpClient.get('/o/kb/ask', { params }),
'Failed to execute request to /o/kb/ask'
);

return {
content: [
{
type: 'text',
text: `Knowledge base answer (${response.data?.mode || 'unknown'} mode):\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
}

// ============================================================================
// KNOWLEDGE_BASE_WRITE TOOL
// ============================================================================

export const writeKnowledgeBasePageToolDefinition = {
name: 'knowledge_base_write',
description: 'Record documentation or decisions into the Countly knowledge base from Markdown via /i/kb/page-write. The server converts the Markdown to sanitized page content and publishes it. Pass a stable external_ref (feature id, branch, ticket) so repeated calls update the same page instead of creating duplicates. Requires create rights on the target space.',
inputSchema: {
type: 'object',
properties: {
space_id: { type: 'string', description: 'Target space id. Call knowledge_base_spaces first if unknown.' },
markdown: { type: 'string', description: 'Page body as Markdown (headings, lists, tables, fenced code).' },
title: { type: 'string', description: 'Page title. Required when creating; optional when updating an existing page by external_ref.' },
external_ref: { type: 'string', description: 'Stable key (feature/branch/ticket id) for idempotent upsert. Same ref updates the same page on later calls; omit and every call creates a new page.' },
parent_id: { type: 'string', description: 'Optional parent page id to nest the new page under.' },
},
required: ['space_id', 'markdown'],
},
};

export async function handleWriteKnowledgeBasePage(context: ToolContext, args: any): Promise<ToolResult> {
const { space_id, markdown, title, external_ref, parent_id } = args;

const params: any = {
...context.getAuthParams(),
space_id,
markdown,
};
if (title) {
params.title = title;
}
if (external_ref) {
params.external_ref = external_ref;
}
if (parent_id) {
params.parent_id = parent_id;
}

const response = await safeApiCall(
() => context.httpClient.post('/i/kb/page-write', null, { params }),
'Failed to execute request to /i/kb/page-write'
);

const created = response.data?.created;
const action = created === false ? 'updated' : 'created';

return {
content: [
{
type: 'text',
text: `Knowledge base page ${action}:\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
}

// ============================================================================
// EXPORTS
// ============================================================================

export const knowledgeBaseToolDefinitions = [
listKnowledgeBaseSpacesToolDefinition,
searchKnowledgeBaseToolDefinition,
askKnowledgeBaseToolDefinition,
writeKnowledgeBasePageToolDefinition,
];

export const knowledgeBaseToolHandlers = {
'knowledge_base_spaces': 'listSpaces',
'knowledge_base_search': 'searchKnowledgeBase',
'knowledge_base_ask': 'askKnowledgeBase',
'knowledge_base_write': 'writePage',
} as const;

export class KnowledgeBaseTools {
constructor(private context: ToolContext) {}

async listSpaces(args: any): Promise<ToolResult> {
return handleListKnowledgeBaseSpaces(this.context, args);
}

async searchKnowledgeBase(args: any): Promise<ToolResult> {
return handleSearchKnowledgeBase(this.context, args);
}

async askKnowledgeBase(args: any): Promise<ToolResult> {
return handleAskKnowledgeBase(this.context, args);
}

async writePage(args: any): Promise<ToolResult> {
return handleWriteKnowledgeBasePage(this.context, args);
}
}

// Metadata for dynamic routing (must be after class declaration)
export const knowledgeBaseToolMetadata = {
instanceKey: 'knowledgeBase',
toolClass: KnowledgeBaseTools,
handlers: knowledgeBaseToolHandlers,
} as const;
Loading
Loading