Files
Orchestrator/bahn/db-planet-mcp-server/.kiro/specs/dbplanet-mcp-tools/design.md
T
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

24 KiB

Design Document: DB Planet MCP Tools Refactoring

Overview

This design describes the refactoring of the DB Planet MCP Server from a single monolithic SearchDBPlanet tool into ~27 focused, single-responsibility MCP tools organized across 8 capability areas. The current architecture creates a new HaiiloClient per tool invocation and bundles search + full content fetching into one tool. The refactored architecture shares a single HaiiloClient per session, separates concerns into individual tools, and adds consistent error handling with 401 retry logic.

The server continues to use StreamableHTTP transport on the existing /servers/dbplanet/mcp endpoint, preserving backward compatibility for transport and authentication.

Architecture

High-Level Structure

graph TD
    A[Express Server<br/>server.ts] -->|creates per session| B[McpServer<br/>getToolServer.ts]
    A -->|extracts credentials| C[HaiiloClient<br/>haiiloClient.ts<br/>shared per session]
    B -->|registers tools from| D[Tool Registry Modules<br/>src/tools/]
    
    D --> D1[searchTools.ts]
    D --> D2[workspaceTools.ts]
    D --> D3[newsTools.ts]
    D --> D4[peopleTools.ts]
    D --> D5[eventTools.ts]
    D --> D6[socialTools.ts]
    D --> D7[notificationTools.ts]
    D --> D8[pageTools.ts]
    D --> D9[contentTools.ts]
    
    D1 --> C
    D2 --> C
    D3 --> C
    D4 --> C
    D5 --> C
    D6 --> C
    D7 --> C
    D8 --> C
    D9 --> C
    
    C -->|OAuth2 + HTTP| E[Haiilo REST API]

Key Design Decisions

  1. One HaiiloClient per session, not per tool call. Currently registerHaiiloTools creates a new HaiiloClient on every invocation. Instead, getToolServer.ts will create the client once and pass it to all registration modules. The client already caches the access token internally.

  2. Tool registration modules by capability area. Each file in src/tools/ registers tools for one capability area. This keeps files small and allows independent evolution. A barrel registerAllTools function in a new src/tools/index.ts calls each module's registration function.

  3. 401 retry in HaiiloClient. The client will intercept 401 responses, clear the cached token, re-authenticate, and retry the request once. This is implemented as an Axios response interceptor.

  4. Consistent error mapping. A shared handleApiError utility maps Axios errors to MCP CallToolResult with isError: true, including the HTTP status code and a human-readable message. All tools use this utility.

  5. Zod schemas with descriptions. Each tool defines its input schema using Zod with .describe() annotations so AI agents understand parameter semantics.

File Structure After Refactoring

src/
├── server.ts                    # Express server (unchanged)
├── getToolServer.ts             # Creates McpServer, HaiiloClient, registers all tools
├── haiiloClient.ts              # HaiiloClient with 401 retry (enhanced) — API client layer
├── errorHandler.ts              # Shared error mapping utility
├── tools/
│   ├── index.ts                 # registerAllTools barrel function
│   ├── searchTools.ts           # Search_FullText, Search_QuickEntity, Search_Grouped
│   ├── workspaceTools.ts        # Workspace_List, Workspace_Get, Workspace_GetMembers, Workspace_Join, Workspace_Create
│   ├── newsTools.ts             # News_GetFeed
│   ├── peopleTools.ts           # People_GetMe, People_List, People_Search
│   ├── eventTools.ts            # Event_Get, Event_GetMemberships, Event_UpdateStatus, Event_Create
│   ├── socialTools.ts           # Social_GetComments, Social_PostComment, Social_GetLikes, Social_ToggleLike
│   ├── notificationTools.ts     # Notification_List, Notification_GetStatus, Notification_MarkSeen
│   ├── pageTools.ts             # Page_List, Page_Get, Page_GetMembers, Page_Create
│   └── contentTools.ts          # Content_GetFull

Components and Interfaces

1. HaiiloClient (Enhanced)

The existing HaiiloClient class is enhanced with:

  • A generic request method for arbitrary API calls (GET, POST, PUT, DELETE)
  • Axios response interceptor for automatic 401 retry with token refresh
  • The existing search, getContentByType, fetchWidgetContent, extractTextFromWidgets, and cleanHtml methods remain for use by Content_GetFull
interface HaiiloCredentials {
  username: string;
  password: string;
  clientId: string;
  clientSecret: string;
  baseUrl: string;
}

class HaiiloClient {
  // Existing fields
  private client: AxiosInstance;
  private credentials: HaiiloCredentials;
  private accessToken?: string;

  constructor(credentials: HaiiloCredentials);

  // New generic request method used by all tools
  async request<T = any>(config: {
    method: 'GET' | 'POST' | 'PUT' | 'DELETE';
    path: string;
    params?: Record<string, any>;
    data?: any;
  }): Promise<T>;

  // Existing methods (kept for Content_GetFull)
  private async getAccessToken(): Promise<string>;
  async search(term: string): Promise<any>;
  private async getContentByType(token: string, item: any): Promise<string>;
  private async fetchWidgetContent(token: string, url: string): Promise<string | null>;
  private extractTextFromWidgets(widgetData: any): string;
  private cleanHtml(text: string): string;
}

The request method:

  1. Calls getAccessToken() to get/reuse the cached token
  2. Makes the HTTP request with Authorization: Bearer {token}
  3. On 401 response: clears accessToken, calls getAccessToken() again, retries once
  4. Returns response.data

2. Error Handler

A shared utility that converts Axios errors into MCP error responses:

// src/tools/errorHandler.ts
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

function handleApiError(error: unknown): CallToolResult;

Mapping:

HTTP Status Message
401 Authentication failed. Check DB Planet credentials.
403 Insufficient permissions to access this resource.
404 The requested resource was not found.
ECONNABORTED / timeout Request timed out.
Other Error: {status} - {statusText}

All error responses follow the shape:

{
  isError: true,
  content: [{
    type: 'text',
    text: `[{status}] {message}`
  }]
}

3. Tool Registration Modules

Each module exports a single function:

type ToolRegistrationFn = (server: McpServer, client: HaiiloClient) => void;

Example — searchTools.ts:

export function registerSearchTools(server: McpServer, client: HaiiloClient): void {
  server.tool(
    'Search_FullText',
    'Full-text search across DB Planet. Returns result metadata (id, title, type, excerpt, modified date) without full content. Use Content_GetFull to fetch full content for a specific result.',
    {
      query: z.string().describe('Search query term'),
      page: z.number().optional().describe('Page number (0-based)'),
      pageSize: z.number().optional().describe('Results per page'),
    },
    async ({ query, page, pageSize }): Promise<CallToolResult> => {
      try {
        const data = await client.request({
          method: 'GET',
          path: '/api/search',
          params: { term: query, _page: page, _pageSize: pageSize },
        });
        // Format and return metadata only
      } catch (error) {
        return handleApiError(error);
      }
    }
  );
  // ... Search_QuickEntity, Search_Grouped
}

4. Barrel Registration (src/tools/index.ts)

export function registerAllTools(server: McpServer, client: HaiiloClient): void {
  registerSearchTools(server, client);
  registerWorkspaceTools(server, client);
  registerNewsTools(server, client);
  registerPeopleTools(server, client);
  registerEventTools(server, client);
  registerSocialTools(server, client);
  registerNotificationTools(server, client);
  registerPageTools(server, client);
  registerContentTools(server, client);
}

5. Updated getToolServer.ts

const getToolAPIServer = async (credentials: HaiiloCredentials) => {
  const server = new McpServer({ name: 'DBPlanet-MCP-Server', version: '0.1.0' }, { capabilities: { logging: {} } });
  
  validateCredentials(credentials);
  const client = new HaiiloClient(credentials);
  registerAllTools(server, client);
  
  return server;
};

Credential validation moves to server creation time. If credentials are missing, the server still starts but tools return descriptive errors on invocation (lazy validation is also acceptable since some tools may not need all credentials).

Complete Tool Inventory

# Tool Name HTTP Method API Path Module
1 Search_FullText GET /api/search searchTools
2 Search_QuickEntity GET /api/quick-entity-search searchTools
3 Search_Grouped GET /api/search/grouped searchTools
4 Workspace_List GET /api/workspaces workspaceTools
5 Workspace_Get GET /api/workspaces/{id} workspaceTools
6 Workspace_GetMembers GET /api/workspaces/{id}/members workspaceTools
7 Workspace_Join PUT /api/workspaces/{id}/members/join workspaceTools
8 Workspace_Create POST /api/workspaces workspaceTools
9 News_GetFeed GET /web/blog/newsfeed newsTools
10 People_GetMe GET /api/users/me peopleTools
11 People_List GET /api/users peopleTools
12 People_Search GET /api/users/chooser/search peopleTools
13 Event_Get GET /api/events/{id} eventTools
14 Event_GetMemberships GET /api/events/{id}/memberships eventTools
15 Event_UpdateStatus PUT /api/events/{id}/memberships/status eventTools
16 Event_Create POST /api/events eventTools
17 Social_GetComments GET /api/comments socialTools
18 Social_PostComment POST /api/comments socialTools
19 Social_GetLikes GET /api/like-targets/{targetType}/{targetId} socialTools
20 Social_ToggleLike POST /api/like-targets/{targetType}/{targetId}/likes/{userId} socialTools
21 Notification_List GET /api/notifications notificationTools
22 Notification_GetStatus GET /api/notifications/status notificationTools
23 Notification_MarkSeen PUT /api/notifications/mark-seen notificationTools
24 Page_List GET /api/pages pageTools
25 Page_Get GET /api/pages/{id} pageTools
26 Page_GetMembers GET /api/pages/{id}/members pageTools
27 Page_Create POST /api/pages pageTools
28 Content_GetFull GET (varies by type) contentTools

Data Models

Input Schemas (Zod)

Each tool defines its input schema using Zod. Below are the schemas grouped by capability area.

Search Tools

// Search_FullText
{
  query: z.string().describe('Search query term'),
  page: z.number().optional().describe('Page number (0-based)'),
  pageSize: z.number().optional().describe('Results per page'),
}

// Search_QuickEntity
{
  query: z.string().describe('Quick search query'),
}

// Search_Grouped
{
  query: z.string().describe('Search query term'),
}

Workspace Tools

// Workspace_List
{} // no parameters

// Workspace_Get
{ id: z.string().describe('Workspace ID') }

// Workspace_GetMembers
{ id: z.string().describe('Workspace ID') }

// Workspace_Join
{ id: z.string().describe('Workspace ID to join') }

// Workspace_Create
{
  name: z.string().describe('Workspace name'),
  description: z.string().optional().describe('Workspace description'),
}

News Tools

// News_GetFeed
{
  page: z.number().optional().describe('Page number (0-based)'),
  pageSize: z.number().optional().describe('Items per page'),
}

People Tools

// People_GetMe
{} // no parameters

// People_List
{
  page: z.number().optional().describe('Page number (0-based)'),
  pageSize: z.number().optional().describe('Users per page'),
}

// People_Search
{
  query: z.string().describe('Search query for people'),
}

Event Tools

// Event_Get
{ id: z.string().describe('Event ID') }

// Event_GetMemberships
{ id: z.string().describe('Event ID') }

// Event_UpdateStatus
{
  id: z.string().describe('Event ID'),
  status: z.string().describe('Attendance status (e.g., accepted, declined, tentative)'),
}

// Event_Create
{
  title: z.string().describe('Event title'),
  description: z.string().optional().describe('Event description'),
  startDate: z.string().describe('Start date/time in ISO 8601 format'),
  endDate: z.string().describe('End date/time in ISO 8601 format'),
  location: z.string().optional().describe('Event location'),
}

Social Tools

// Social_GetComments
{ targetId: z.string().describe('ID of the entity to get comments for') }

// Social_PostComment
{
  targetId: z.string().describe('ID of the entity to comment on'),
  body: z.string().describe('Comment text'),
}

// Social_GetLikes
{
  targetType: z.string().describe('Type of the like target (e.g., blog-article, timeline-item)'),
  targetId: z.string().describe('ID of the like target'),
}

// Social_ToggleLike
{
  targetType: z.string().describe('Type of the like target'),
  targetId: z.string().describe('ID of the like target'),
  userId: z.string().describe('User ID to toggle like for'),
}

Notification Tools

// Notification_List
{
  page: z.number().optional().describe('Page number (0-based)'),
  pageSize: z.number().optional().describe('Notifications per page'),
}

// Notification_GetStatus
{} // no parameters

// Notification_MarkSeen
{} // no parameters

Page Tools

// Page_List
{} // no parameters

// Page_Get
{ id: z.string().describe('Page ID or slug') }

// Page_GetMembers
{ id: z.string().describe('Page ID') }

// Page_Create
{
  title: z.string().describe('Page title'),
  content: z.string().optional().describe('Page content'),
}

Content Tools

// Content_GetFull
{
  contentId: z.string().describe('ID of the content item'),
  contentType: z.string().describe('Content type: blog-article, wiki-article, page, workspace, app, timeline-item'),
  appId: z.string().optional().describe('App ID (required for blog and wiki articles)'),
  slug: z.string().optional().describe('Slug (alternative identifier for pages and workspaces)'),
}

Response Shapes

All tools return CallToolResult from the MCP SDK:

// Success
{
  content: [{ type: 'text', text: string }]
}

// Error
{
  isError: true,
  content: [{ type: 'text', text: string }]
}

Tool responses format data as human-readable text (not JSON) for LLM consumption, consistent with the current SearchDBPlanet tool's approach. Each tool formats its response with clear labels and structure.

HaiiloClient Request/Response

// Generic request config
interface RequestConfig {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  path: string;           // e.g., '/api/search'
  params?: Record<string, any>;  // query parameters
  data?: any;             // request body (POST/PUT)
}

// The client prepends credentials.baseUrl to path and adds Authorization header

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Tool-to-endpoint routing correctness

For any registered tool and any valid input for that tool, the tool SHALL dispatch an HTTP request using the correct method (GET/POST/PUT) to the correct API path with the input parameters correctly mapped to query params or request body.

Validates: Requirements 3.1, 3.3, 3.4, 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 8.3, 8.4, 9.1, 9.3, 9.4, 10.1, 10.2, 10.3, 10.4, 11.1

Property 2: Pagination parameter forwarding

For any tool that accepts optional pagination parameters (page, pageSize), when those parameters are provided, they SHALL be forwarded as query parameters to the underlying API request. When omitted, the API request SHALL not include them.

Validates: Requirements 3.2, 5.2, 6.4, 9.2

Property 3: Error handler maps HTTP status to correct MCP error

For any HTTP error response with a status code (401, 403, 404, timeout, or other), the handleApiError function SHALL produce a CallToolResult with isError: true containing both the HTTP status code and a human-readable message appropriate for that status.

Validates: Requirements 12.1, 12.2, 12.3, 12.4, 12.5

Property 4: Tool metadata completeness

For any registered tool, it SHALL have a name matching the pattern {Area}_{Action}, a non-empty description string, and a Zod input schema.

Validates: Requirements 2.3, 2.4, 2.5

Property 5: Token caching — single authentication per session

For any sequence of N requests made through the same HaiiloClient instance (where N > 1 and no 401 is received), the client SHALL make exactly one OAuth2 token request and reuse the cached token for all subsequent requests.

Validates: Requirements 1.1, 1.2

Property 6: Missing credentials produce error

For any subset of the required credentials (username, password, clientId, clientSecret, baseUrl) where at least one field is missing or empty, invoking any tool SHALL return an MCP error response with a descriptive message about the missing credentials.

Validates: Requirements 1.4

Property 7: Search_FullText returns metadata only

For any search query, the Search_FullText tool's response SHALL contain result metadata (id, title, type, excerpt, modified date) and SHALL NOT contain full rendered content for any result.

Validates: Requirements 3.5

Property 8: HTML widget text extraction removes all tags

For any HTML string input, the cleanHtml function SHALL return a string containing no HTML tags (no <...> sequences), and all HTML entities SHALL be decoded to their character equivalents.

Validates: Requirements 11.3

Property 9: Unsupported content type returns error

For any content type string that is not in the supported set (blog-article, wiki-article, page, workspace, app, timeline-item), the Content_GetFull tool SHALL return an MCP error response indicating the unsupported content type.

Validates: Requirements 11.4

Property 10: 401 retry — re-authenticate and retry once

For any API request that receives a 401 response, the HaiiloClient SHALL clear the cached token, re-authenticate, and retry the request exactly once. If the retry also fails with 401, the error SHALL be propagated.

Validates: Requirements 1.3

Error Handling

Error Flow

graph TD
    A[Tool Handler] -->|try| B[client.request]
    B -->|success| C[Format & Return CallToolResult]
    B -->|401| D[Clear token, re-auth, retry once]
    D -->|success| C
    D -->|fail| E[handleApiError]
    B -->|other error| E
    E -->|401| F["[401] Authentication failed. Check DB Planet credentials."]
    E -->|403| G["[403] Insufficient permissions to access this resource."]
    E -->|404| H["[404] The requested resource was not found."]
    E -->|timeout| I["[TIMEOUT] Request timed out."]
    E -->|other| J["[{status}] {statusText or message}"]

Error Response Contract

Every tool handler wraps its logic in try/catch and delegates to handleApiError:

async (params): Promise<CallToolResult> => {
  try {
    const data = await client.request({ method, path, params });
    return { content: [{ type: 'text', text: formatResponse(data) }] };
  } catch (error) {
    return handleApiError(error);
  }
}

The handleApiError function:

  1. Checks if the error is an Axios error with a response status
  2. Maps known status codes to human-readable messages
  3. Handles network errors (ECONNABORTED, ECONNREFUSED, etc.)
  4. Always returns { isError: true, content: [{ type: 'text', text }] }

Credential Validation

Credentials are validated lazily — when the first tool is invoked and client.request() calls getAccessToken(). If credentials are missing, the OAuth request fails and the error propagates through handleApiError. This avoids blocking server startup when credentials might be provided later via headers.

401 Retry Logic

Implemented as an Axios response interceptor on the HaiiloClient:

this.client.interceptors.response.use(
  response => response,
  async (error) => {
    if (error.response?.status === 401 && !error.config._retried) {
      error.config._retried = true;
      this.accessToken = undefined;
      const token = await this.getAccessToken();
      error.config.headers['Authorization'] = `Bearer ${token}`;
      return this.client.request(error.config);
    }
    return Promise.reject(error);
  }
);

The _retried flag prevents infinite retry loops.

Testing Strategy

Property-Based Testing Library

Use fast-check (fc) for TypeScript property-based testing. It integrates well with the existing TypeScript/ESM setup and supports custom arbitraries for generating tool inputs.

npm install --save-dev fast-check vitest @types/node

Test Structure

tests/
├── properties/
│   ├── toolRouting.property.test.ts    # Property 1: Tool-to-endpoint routing
│   ├── pagination.property.test.ts     # Property 2: Pagination forwarding
│   ├── errorHandler.property.test.ts   # Property 3: Error mapping
│   ├── toolMetadata.property.test.ts   # Property 4: Tool metadata
│   ├── tokenCaching.property.test.ts   # Property 5: Token caching
│   ├── credentials.property.test.ts    # Property 6: Missing credentials
│   ├── searchMetadata.property.test.ts # Property 7: Search metadata only
│   ├── htmlExtraction.property.test.ts # Property 8: HTML extraction
│   ├── contentType.property.test.ts    # Property 9: Unsupported content type
│   └── retryLogic.property.test.ts     # Property 10: 401 retry
├── unit/
│   ├── errorHandler.test.ts            # Specific error code examples
│   ├── haiiloClient.test.ts            # Client construction, auth flow
│   ├── contentTools.test.ts            # Content type routing examples
│   └── registration.test.ts            # All tools registered, backward compat

Property-Based Testing Approach

Each property test:

  • Runs a minimum of 100 iterations
  • Uses fast-check arbitraries to generate random inputs
  • References the design property in a comment tag
  • Mocks the Axios client to verify request dispatch without hitting real APIs

Example property test structure:

import fc from 'fast-check';
import { describe, it, expect } from 'vitest';

describe('Tool-to-endpoint routing', () => {
  // Feature: dbplanet-mcp-tools, Property 1: Tool-to-endpoint routing correctness
  it('dispatches to correct HTTP method and path for any valid input', () => {
    fc.assert(
      fc.property(
        toolInputArbitrary, // generates { toolName, input } pairs
        ({ toolName, input }) => {
          // Mock client.request, invoke tool, verify method + path
          const expectedRoute = TOOL_ROUTE_MAP[toolName];
          // ... assertion
        }
      ),
      { numRuns: 100 }
    );
  });
});

Unit Testing Approach

Unit tests cover:

  • Specific examples for each content type in Content_GetFull (Req 11.2)
  • Backward compatibility: server listens on port 3000, /health returns 200, headers accepted (Req 13.1-13.4)
  • All 28 tools registered after initialization (Req 2.2)
  • 401 retry with mock returning 401 then 200 (Req 1.3)
  • Content fetch failure returns descriptive error (Req 11.5)

Mocking Strategy

  • Mock axios.create() to return a mock Axios instance
  • Mock the OAuth token endpoint to return a test token
  • Mock API endpoints to return fixture data
  • Use Axios interceptor spies to verify retry behavior

Test Configuration

Each property-based test must include:

  • Comment tag: // Feature: dbplanet-mcp-tools, Property {N}: {title}
  • Minimum 100 iterations: { numRuns: 100 }
  • Clear assertion messages for debugging failures