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.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,66 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AxiosError } from "axios";
export function handleApiError(error: unknown): CallToolResult {
if (error instanceof AxiosError) {
if (error.code === "ECONNABORTED") {
return {
isError: true,
content: [{ type: "text", text: "[TIMEOUT] Request timed out." }],
};
}
const status = error.response?.status;
if (status === 401) {
return {
isError: true,
content: [
{
type: "text",
text: "[401] Authentication failed. Check DB Planet credentials.",
},
],
};
}
if (status === 403) {
return {
isError: true,
content: [
{
type: "text",
text: "[403] Insufficient permissions to access this resource.",
},
],
};
}
if (status === 404) {
return {
isError: true,
content: [
{
type: "text",
text: "[404] The requested resource was not found.",
},
],
};
}
if (status) {
const message = error.response?.statusText || error.message;
return {
isError: true,
content: [{ type: "text", text: `[${status}] ${message}` }],
};
}
}
const message =
error instanceof Error ? error.message : "An unknown error occurred";
return {
isError: true,
content: [{ type: "text", text: `[ERROR] ${message}` }],
};
}
@@ -0,0 +1,39 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { HaiiloClient } from "./haiiloClient.js";
import { registerAllTools } from "./tools/index.js";
import { Logger } from 'tslog';
const log = new Logger();
interface HaiiloCredentials {
username?: string;
password?: string;
clientId?: string;
clientSecret?: string;
baseUrl?: string;
}
const getToolAPIServer = async (credentials: HaiiloCredentials) => {
const server = new McpServer({
name: 'DBPlanet-MCP-Server',
version: '0.1.0',
}, {
capabilities: {
logging: {}
}
});
const client = new HaiiloClient({
username: credentials.username || '',
password: credentials.password || '',
clientId: credentials.clientId || '',
clientSecret: credentials.clientSecret || '',
baseUrl: credentials.baseUrl || '',
});
registerAllTools(server, client);
return server;
};
export default getToolAPIServer;
@@ -0,0 +1,375 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { Logger } from 'tslog';
import { HttpsProxyAgent } from 'https-proxy-agent';
// Extend Axios request config to support the _retried flag
declare module 'axios' {
interface InternalAxiosRequestConfig {
_retried?: boolean;
}
}
const log = new Logger();
export interface HaiiloCredentials {
username: string;
password: string;
clientId: string;
clientSecret: string;
baseUrl: string;
}
export class HaiiloClient {
private client: AxiosInstance;
private credentials: HaiiloCredentials;
private accessToken?: string;
constructor(credentials: HaiiloCredentials) {
this.credentials = credentials;
const axiosConfig: any = {
timeout: 60000,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
};
// Configure proxy for HTTPS tunneling via CONNECT
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
const proxyUrl = (process.env.HTTPS_PROXY || process.env.HTTP_PROXY)!;
log.info('Using proxy:', proxyUrl);
axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl);
axiosConfig.proxy = false;
}
this.client = axios.create(axiosConfig);
// 401 retry interceptor: on 401, clear token, re-authenticate, retry once.
// Skip retry for token requests to avoid infinite loops.
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const isTokenRequest = error.config?.url?.includes('/api/oauth/token');
if (error.response?.status === 401 && !error.config._retried && !isTokenRequest) {
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);
}
);
}
private async getAccessToken(): Promise<string> {
if (this.accessToken) return this.accessToken;
if (!this.credentials.baseUrl) {
throw new Error('OAuth token request failed: baseUrl is not configured. Set the x-dbplanet-base-url header.');
}
const basicAuth = Buffer.from(`${this.credentials.clientId}:${this.credentials.clientSecret}`).toString('base64');
const tokenUrl = `${this.credentials.baseUrl}/api/oauth/token`;
try {
const params = new URLSearchParams();
params.append('grant_type', 'password');
params.append('username', this.credentials.username);
params.append('password', this.credentials.password);
const response = await this.client.post(
tokenUrl,
params.toString(),
{
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 120000
}
);
this.accessToken = response.data.access_token;
log.info('OAuth authentication successful');
return this.accessToken!;
} catch (error: any) {
const errMsg = error.code === 'ECONNABORTED'
? `timeout after 120s`
: `${error.response?.status || error.code} - ${error.response?.statusText || error.message}`;
log.error('OAuth authentication failed:', errMsg);
throw new Error(`OAuth token request failed: ${errMsg}`);
}
}
async getToken(): Promise<string> {
return this.getAccessToken();
}
getBaseUrl(): string {
return this.credentials.baseUrl;
}
async request<T = any>(config: {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
params?: Record<string, any>;
data?: any;
}): Promise<T> {
log.info(`[REQUEST] ${config.method} ${config.path} starting...`);
const start = Date.now();
try {
const token = await this.getAccessToken();
log.info(`[REQUEST] ${config.method} ${config.path} token acquired in ${Date.now() - start}ms`);
const response = await this.client.request<T>({
method: config.method,
url: `${this.credentials.baseUrl}${config.path}`,
params: config.params,
data: config.data,
headers: {
'Authorization': `Bearer ${token}`,
},
});
log.info(`[REQUEST] ${config.method} ${config.path} completed in ${Date.now() - start}ms, status=${response.status}`);
return response.data;
} catch (error: any) {
const errMsg = error.code === 'ECONNABORTED'
? `timeout after ${Date.now() - start}ms`
: `${error.response?.status || error.code} - ${error.response?.statusText || error.message}`;
log.error(`[REQUEST] ${config.method} ${config.path} failed after ${Date.now() - start}ms: ${errMsg}`);
throw error;
}
}
async search(term: string): Promise<any> {
const token = await this.getAccessToken();
const searchUrl = `${this.credentials.baseUrl}/api/search/`;
try {
const response = await this.client.get(searchUrl, {
headers: { 'Authorization': `Bearer ${token}` },
params: { term }
});
const items = response.data.content || [];
const results = [];
for (const item of items) {
const cleanExcerpt = this.cleanHtml(item.excerpt || '');
const result: any = {
id: item.id,
source: item.sender?.displayName || 'Unknown',
type: item.typeName || 'Unknown',
title: this.cleanHtml(item.displayName || 'Untitled'),
snippet: cleanExcerpt,
modified_date: item.modified
};
const contentType = (item.typeName || '').toLowerCase();
if (!contentType.includes('timeline')) {
try {
const content = await this.getContentByType(token, item);
result.fullContent = content;
} catch (error) {
result.fullContent = cleanExcerpt;
}
} else {
result.fullContent = cleanExcerpt;
}
results.push(result);
}
return { total_results: results.length, results };
} catch (error: any) {
log.error('Search failed:', error.message);
throw new Error(`Search failed: ${error.response?.status} ${error.response?.statusText}`);
}
}
async getContentByType(token: string, item: any): Promise<string> {
const contentType = (item.typeName || '').toLowerCase();
const contentId = item.id;
const targetParams = item.target?.params || {};
// Blog articles
if (contentType.includes('blog') || contentType.includes('article')) {
const appId = targetParams.appId;
if (appId) {
const layoutName = `app-blog-${appId}-${contentId}`;
const url = `${this.credentials.baseUrl}/api/blog-article/${contentId}/widgets/${layoutName}`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
}
// Standalone app content
if (contentType === 'app') {
const layoutName = `app-content-${contentId}`;
const url = `${this.credentials.baseUrl}/api/app/${contentId}/widgets/${layoutName}`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
// Pages
if (contentType.includes('page')) {
const slug = targetParams.slug || item.slug;
const pageId = targetParams.id || contentId;
if (slug) {
const url = `${this.credentials.baseUrl}/api/pages/${slug}/widgets/default`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
const url = `${this.credentials.baseUrl}/api/pages/${pageId}/widgets/default`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
// Workspaces
if (contentType.includes('workspace')) {
const slug = targetParams.slug || item.slug;
const workspaceId = targetParams.id || contentId;
if (slug) {
const url = `${this.credentials.baseUrl}/api/workspaces/${slug}/widgets/default`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
const url = `${this.credentials.baseUrl}/api/workspaces/${workspaceId}/widgets/default`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
// Timeline items
if (contentType.includes('timeline')) {
const senderId = item.sender?.id;
if (senderId) {
const url = `${this.credentials.baseUrl}/api/timeline-items`;
try {
const response = await this.client.get(url, {
headers: { 'Authorization': `Bearer ${token}` },
params: { type: 'sender', senderId, _page: 0, _pageSize: 100 }
});
const timelineItem = response.data.content?.find((ti: any) => ti.id === contentId);
if (timelineItem?.data?.message) {
const msg = timelineItem.data.message;
let cleanMsg = msg.replace(/<[^>]+>/g, ' ');
cleanMsg = this.cleanHtml(cleanMsg);
cleanMsg = cleanMsg.replace(/ +/g, ' ').trim();
return cleanMsg;
}
} catch (error: any) {
log.warn(`Timeline items endpoint failed for ${contentId}: ${error.message}`);
}
// Fallback: try direct timeline item endpoint
try {
const directUrl = `${this.credentials.baseUrl}/api/timeline-items/${contentId}`;
const response = await this.client.get(directUrl, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.data?.data?.message) {
const msg = response.data.data.message;
let cleanMsg = msg.replace(/<[^>]+>/g, ' ');
cleanMsg = this.cleanHtml(cleanMsg);
cleanMsg = cleanMsg.replace(/ +/g, ' ').trim();
return cleanMsg;
}
} catch (error: any) {
log.warn(`Direct timeline endpoint failed for ${contentId}: ${error.message}`);
}
}
}
// Wiki articles
if (contentType.includes('wiki')) {
const appId = targetParams.appId;
if (appId) {
// Try to get wiki article metadata first to find version
try {
const metaUrl = `${this.credentials.baseUrl}/api/wiki-article/${contentId}`;
const metaResp = await this.client.get(metaUrl, {
headers: { 'Authorization': `Bearer ${token}` }
});
const version = metaResp.data.version || 1;
const layoutName = `app-wiki-${appId}-${contentId}-${version}`;
const url = `${this.credentials.baseUrl}/api/wiki-article/${contentId}/widgets/${layoutName}`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
} catch (error: any) {
log.warn(`Wiki metadata fetch failed for ${contentId}: ${error.message}`);
}
// Fallback: try multiple versions
for (const version of [1, 2, 3, 4, 5]) {
const layoutName = `app-wiki-${appId}-${contentId}-${version}`;
const url = `${this.credentials.baseUrl}/api/wiki-article/${contentId}/widgets/${layoutName}`;
const content = await this.fetchWidgetContent(token, url);
if (content) return content;
}
}
}
throw new Error('Content retrieval failed for type: ' + contentType);
}
async fetchWidgetContent(token: string, url: string): Promise<string | null> {
try {
const response = await this.client.get(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
return this.extractTextFromWidgets(response.data);
} catch (error: any) {
log.debug(`Widget endpoint failed: ${error.message}`);
return null;
}
}
extractTextFromWidgets(widgetData: any): string {
const textParts: string[] = [];
const rows = widgetData.rows || [];
for (const row of rows) {
for (const column of row) {
for (const widget of column.widgets || []) {
if (widget.key === 'rte') {
const htmlContent = widget.settings?.html_content || '';
// Remove HTML tags but preserve structure
let cleanText = htmlContent.replace(/<[^>]+>/g, ' ');
cleanText = this.cleanHtml(cleanText);
// Clean up excessive whitespace but preserve line breaks
cleanText = cleanText.replace(/ +/g, ' ');
cleanText = cleanText.replace(/\n\s*\n/g, '\n\n');
cleanText = cleanText.trim();
if (cleanText) textParts.push(cleanText);
} else if (widget.key === 'teaser') {
const slides = widget.settings?.slides || [];
for (const slide of slides) {
const headline = slide.headline || '';
if (headline) textParts.push(`[${headline}]`);
}
}
}
}
}
return textParts.join('\n\n');
}
cleanHtml(text: string): string {
return text
.replace(/<[^>]*>/g, '')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
}
+164
View File
@@ -0,0 +1,164 @@
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express, { Request, Response, NextFunction } from 'express';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
import getToolAPIServer from "./getToolServer.js";
import bodyParser from 'body-parser';
import { Logger } from "tslog";
import cors from 'cors';
// Load .env file manually (no dotenv dependency)
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const envPath = resolve(__dirname, '..', '.env');
const envContent = readFileSync(envPath, 'utf-8');
for (const line of envContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
} catch (e) {
// .env file not found — rely on environment variables
}
const log = new Logger({stylePrettyLogs: false});
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors<Request>());
function getCredentials(req: Request): {
username?: string;
password?: string;
clientId?: string;
clientSecret?: string;
baseUrl?: string
} {
return {
username: (req.headers["x-dbplanet-username"] as string) || process.env.DBPLANET_USERNAME,
password: (req.headers["x-dbplanet-password"] as string) || process.env.DBPLANET_PASSWORD,
clientId: (req.headers["x-dbplanet-client-id"] as string) || process.env.DBPLANET_CLIENT_ID,
clientSecret: (req.headers["x-dbplanet-client-secret"] as string) || process.env.DBPLANET_CLIENT_SECRET,
baseUrl: (req.headers["x-dbplanet-base-url"] as string) || process.env.DBPLANET_BASE_URL
};
}
const transports: Record<string, StreamableHTTPServerTransport> = {};
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'ok' });
});
app.all('/servers/dbplanet/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
log.info(`Request to /mcp, session=${sessionId ?? "n/a"}, method=${req.method}`);
try {
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore,
onsessioninitialized: (sessionId) => {
log.info('StreamableHTTP session initialized, session=', sessionId);
transports[sessionId] = transport;
}
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
log.info(`Transport closed, session=${sessionId}`);
delete transports[sid];
}
};
const credentials = getCredentials(req);
const server = await getToolAPIServer(credentials);
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// For GET requests (SSE streams), send periodic keep-alive pings
// to prevent client-side read timeouts on idle connections.
let keepAliveInterval: ReturnType<typeof setInterval> | undefined;
if (req.method === 'GET') {
keepAliveInterval = setInterval(() => {
if (!res.writableEnded) {
res.write(':ping\n\n');
} else {
clearInterval(keepAliveInterval);
}
}, 15_000);
res.on('close', () => {
if (keepAliveInterval) clearInterval(keepAliveInterval);
});
}
await transport.handleRequest(req, res, req.body);
} catch (error) {
log.error('Error handling MCP request:', (error as Error).message);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
log.error('Server error:', err.message);
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'production' ? undefined : err.message
});
});
const PORT = 4000;
app.listen(PORT, () => {
log.info(`DB Planet MCP server listening on port ${PORT}`);
log.info(`\nEndpoint: /servers/dbplanet/mcp\nMethods: GET, POST, DELETE\n`);
});
process.on('SIGINT', async () => {
log.info('Shutting down server...');
for (const sessionId in transports) {
try {
log.info(`Closing transport, session=${sessionId}`);
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
log.error(`Error closing transport, session=${sessionId}:`, (error as Error).message);
}
}
log.info('Server shutdown complete');
process.exit(0);
});
@@ -0,0 +1,67 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
const SUPPORTED_TYPES = ["blog-article", "wiki-article", "page", "workspace", "app", "timeline-item"];
export function registerContentTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Content_GetFull",
"Fetch the full rendered content of a specific item by its ID and type. Use ONLY for blog-article and wiki-article types — these are the only types with fetchable full content. Do NOT use for workspace, page, event, or timeline-item — use Workspace_Get, Page_Get, or Event_Get instead for those.",
{
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)"),
senderId: z.string().optional().describe("Sender/page ID (the page or workspace that contains this content)"),
slug: z.string().optional().describe("Slug (alternative identifier for pages and workspaces)"),
},
async ({ contentId, contentType, appId, senderId, slug }: any): Promise<CallToolResult> => {
try {
if (!SUPPORTED_TYPES.includes(contentType)) {
return {
isError: true,
content: [{ type: "text", text: `Unsupported content type: "${contentType}". Supported types: ${SUPPORTED_TYPES.join(", ")}` }],
};
}
const item = {
id: contentId,
typeName: contentType,
target: { params: { appId, slug, id: contentId, senderId } },
sender: { id: senderId || contentId },
slug,
};
const fullContent = await client.getContentByType(
await client.getToken(),
item
);
// Build link if possible
const baseUrl = client.getBaseUrl().replace(/\/+$/, '');
let link = '';
const ct = contentType.toLowerCase();
if (ct === 'blog-article' && senderId && appId) {
link = `${baseUrl}/pages/${senderId}/apps/blog/${appId}/view/${contentId}`;
} else if (ct === 'workspace') {
link = slug ? `${baseUrl}/workspaces/${slug}` : `${baseUrl}/workspaces/${contentId}`;
} else if (ct === 'page') {
link = slug ? `${baseUrl}/pages/${slug}` : `${baseUrl}/pages/${contentId}`;
}
let text = fullContent;
if (link) {
text += `\n\n[Auf DB Planet ansehen](${link})`;
}
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,94 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerEventTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Event_Get",
"Get event details by ID. NOT for discovering or listing events — use Search_FullText or Search_QuickEntity first to find events, then call this tool with the returned event UUID.",
{ id: z.string().describe("Event UUID (use Search_FullText or Search_QuickEntity to find it)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/events/${id}` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Event_GetMemberships",
"Get attendees/participants for a specific event. NOT for discovering or listing events — use Search_FullText or Search_QuickEntity first to find events. The id must be an EVENT UUID, not a user or workspace ID. Only works if the event has showParticipants enabled; otherwise returns 404.",
{ id: z.string().describe("Event UUID (NOT a user or workspace ID — use Search_FullText or Search_QuickEntity to find events first)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/events/${id}/memberships` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Event_UpdateStatus",
"Update the current user's attendance status for a specific event. NOT for discovering or listing events — use Search_FullText or Search_QuickEntity first to find events. Use this to accept (ATTENDING), decline/cancel (NOT_ATTENDING), or mark as tentative (MAYBE_ATTENDING). Requires the event UUID.",
{
id: z.string().describe("Event UUID (use Search_FullText or Search_QuickEntity to find it)"),
status: z.string().describe("Attendance status. Valid values: ATTENDING, NOT_ATTENDING, MAYBE_ATTENDING"),
},
async ({ id, status }: any): Promise<CallToolResult> => {
try {
await client.request<any>({ method: "PUT", path: `/api/events/${id}/status`, data: { status } });
return { content: [{ type: "text", text: `Attendance status updated to "${status}" for event ${id}.` }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Event_Create",
"Create a new event on DB Planet.",
{
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"),
},
async ({ title, description, startDate, endDate, location }: any): Promise<CallToolResult> => {
try {
const meData = await client.request<any>({ method: "GET", path: "/api/users/me" });
const data = await client.request<any>({
method: "POST",
path: "/api/events",
data: {
name: title,
description: description || "",
place: location || null,
startDate,
endDate,
fullDay: false,
public: true,
showParticipants: true,
requestDefiniteAnswer: false,
hostId: { id: meData.id, typeName: "user" },
adminIds: [meData.id],
},
});
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,23 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { HaiiloClient } from "../haiiloClient.js";
import { registerSearchTools } from "./searchTools.js";
import { registerWorkspaceTools } from "./workspaceTools.js";
import { registerNewsTools } from "./newsTools.js";
import { registerPeopleTools } from "./peopleTools.js";
import { registerEventTools } from "./eventTools.js";
import { registerSocialTools } from "./socialTools.js";
import { registerNotificationTools } from "./notificationTools.js";
import { registerPageTools } from "./pageTools.js";
import { registerContentTools } from "./contentTools.js";
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);
}
@@ -0,0 +1,35 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerNewsTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"News_GetFeed",
"Retrieve the news feed from DB Planet. Returns blog articles from pages the user is subscribed to.",
{
includePublished: z.boolean().optional().describe("Filter for published articles, default true"),
includeScheduled: z.boolean().optional().describe("Filter for scheduled articles, default false"),
includeDrafts: z.boolean().optional().describe("Filter for draft articles, default false"),
limitDate: z.string().optional().describe("Limit results to time frame, format YYYY-MM or YYYY"),
},
async ({ includePublished, includeScheduled, includeDrafts, limitDate }: any): Promise<CallToolResult> => {
try {
const params: Record<string, any> = {};
if (includePublished !== undefined) params.includePublished = includePublished;
if (includeScheduled !== undefined) params.includeScheduled = includeScheduled;
if (includeDrafts !== undefined) params.includeDrafts = includeDrafts;
if (limitDate !== undefined) params.limitDate = limitDate;
const data = await client.request<any>({ method: "GET", path: "/web/blog/newsfeed", params });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,66 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerNotificationTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Notification_List",
"List notifications with their read/unread status.",
{
category: z.string().optional().describe("Notification category (e.g., ACTIVITY, DISCUSSION)"),
page: z.number().optional().describe("Page number (0-based)"),
pageSize: z.number().optional().describe("Notifications per page"),
},
async ({ category, page, pageSize }: any): Promise<CallToolResult> => {
try {
const params: Record<string, any> = {};
if (category !== undefined) params.category = category;
if (page !== undefined) params._page = page;
if (pageSize !== undefined) params._pageSize = pageSize;
const data = await client.request<any>({ method: "GET", path: "/api/notifications", params });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Notification_GetStatus",
"Get the count of unseen notifications.",
{},
async (_: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: "/api/notifications/status" });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Notification_MarkSeen",
"Mark all notifications as seen.",
{
category: z.string().optional().describe("Notification category to mark as seen (e.g., ACTIVITY, DISCUSSION). If omitted, marks all."),
},
async ({ category }: any): Promise<CallToolResult> => {
try {
const params: Record<string, any> = {};
if (category !== undefined) params.category = category;
await client.request<any>({ method: "PUT", path: "/api/notifications/action/mark-seen", params });
return { content: [{ type: "text", text: "Notifications marked as seen." }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,87 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerPageTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Page_List",
"List available pages on DB Planet.",
{},
async (_: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: "/api/pages" });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Page_Get",
"Get page details by ID or slug. Use Page_List or Search_QuickEntity to find the ID first.",
{ id: z.string().describe("Page UUID or slug") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/pages/${id}` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Page_GetMembers",
"Get members and roles for a specific page. NOT for finding or listing pages — use Page_List or Search_QuickEntity first to find the page ID.",
{ id: z.string().describe("Page UUID (use Page_List or Search_QuickEntity to find it)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/pages/${id}/members` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Page_Create",
"Create a new page on DB Planet.",
{
title: z.string().describe("Page title"),
content: z.string().optional().describe("Page content"),
visibility: z.string().optional().describe("Visibility: PUBLIC or PRIVATE. Defaults to PUBLIC."),
defaultLanguage: z.string().optional().describe("Default language (e.g., EN, DE). Defaults to EN."),
},
async ({ title, content, visibility, defaultLanguage }: any): Promise<CallToolResult> => {
try {
const meData = await client.request<any>({ method: "GET", path: "/api/users/me" });
const data = await client.request<any>({
method: "POST",
path: "/api/pages",
data: {
id: null,
slug: null,
name: title,
visibility: visibility || "PUBLIC",
defaultLanguage: defaultLanguage || "EN",
adminIds: [meData.id],
translations: {},
},
});
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,118 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerPeopleTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"People_GetMe",
"Get the current authenticated user's profile information.",
{},
async (_: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: "/api/users/me" });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"People_Get",
"Get a user's profile by their UUID. NOT for searching or finding people — use People_Search (by name) or Search_QuickEntity first to find the user ID.",
{ id: z.string().describe("User UUID (use People_Search or Search_QuickEntity to find it)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/users/${id}` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"People_List",
"List users on DB Planet with optional pagination.",
{
page: z.number().optional().describe("Page number (0-based)"),
pageSize: z.number().optional().describe("Users per page"),
},
async ({ page, pageSize }: any): Promise<CallToolResult> => {
try {
const params: Record<string, any> = {};
if (page !== undefined) params._page = page;
if (pageSize !== undefined) params._pageSize = pageSize;
const data = await client.request<any>({ method: "GET", path: "/api/users", params });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"People_Search",
"Search for people on DB Planet by name. This tool ONLY matches on user names — it cannot search by department, job title, or expertise. For topic-based people discovery, use Search_FullText instead.",
{ query: z.string().describe("Search query for people") },
async ({ query }: any): Promise<CallToolResult> => {
try {
const cleanQuery = query.replace(/^["']+|["']+$/g, '');
const data = await client.request<any>({ method: "GET", path: "/api/users/chooser/search", params: { term: cleanQuery, types: "user" } });
const items = Array.isArray(data) ? data : (data?.content || data?.results || []);
if (items.length === 0) {
return { content: [{ type: "text", text: `No people found matching "${query}". This tool only searches by name. Try Search_FullText to find people by topic, expertise, or content they authored.` }] };
}
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"People_GetManaged",
"Get all users that a given user manages (direct reports). Use People_Search or People_GetMe to find the user ID first. Returns an empty list if the user doesn't manage anyone.",
{
id: z.string().describe("User UUID of the manager"),
page: z.number().optional().describe("Page number (0-based)"),
pageSize: z.number().optional().describe("Results per page"),
},
async ({ id, page, pageSize }: any): Promise<CallToolResult> => {
try {
const params: Record<string, any> = {};
if (page !== undefined) params._page = page;
if (pageSize !== undefined) params._pageSize = pageSize;
const data = await client.request<any>({ method: "GET", path: `/api/users/${id}/managed`, params });
const items = data?.content || (Array.isArray(data) ? data : []);
if (items.length === 0) {
return { content: [{ type: "text", text: "This user does not manage any other users." }] };
}
let text = `This user manages ${items.length} direct report(s):\n\n`;
items.forEach((user: any, i: number) => {
text += `[${i + 1}] ${user.displayName || user.firstname + ' ' + user.lastname || "Unknown"}\n`;
text += ` ID: ${user.id}\n`;
if (user.email) text += ` Email: ${user.email}\n`;
if (user.properties?.department) text += ` Department: ${user.properties.department}\n`;
if (user.properties?.jobTitle) text += ` Job Title: ${user.properties.jobTitle}\n`;
text += '\n';
});
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,155 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
function buildLink(baseUrl: string, item: any): string {
const targetParams = item.target?.params || {};
const typeName = (item.typeName || "").toLowerCase();
const senderId = targetParams.senderId || item.sender?.id || '';
if (typeName === 'blog-article' || typeName.includes('blog')) {
const appId = targetParams.appId || '';
if (senderId && appId) return `${baseUrl}/pages/${senderId}/apps/blog/${appId}/view/${item.id}`;
} else if (typeName === 'wiki-article' || typeName.includes('wiki')) {
// Wiki article URL pattern unclear — skip link generation
return '';
} else if (typeName === 'workspace') {
const slug = targetParams.slug || '';
return slug ? `${baseUrl}/workspaces/${slug}` : `${baseUrl}/workspaces/${item.id}`;
} else if (typeName === 'page') {
const slug = targetParams.slug || '';
return slug ? `${baseUrl}/pages/${slug}` : `${baseUrl}/pages/${item.id}`;
} else if (typeName === 'event') {
const slug = targetParams.slug || '';
return slug ? `${baseUrl}/events/${slug}` : `${baseUrl}/events/${item.id}`;
} else if (typeName === 'app') {
const senderSlug = targetParams.senderSlug || item.sender?.slug || '';
if (senderSlug) return `${baseUrl}/pages/${senderSlug}/apps/content/${item.id}`;
} else if (typeName === 'user') {
const slug = targetParams.slug || '';
return slug ? `${baseUrl}/users/${slug}` : '';
} else if (typeName === 'file') {
// File URLs require an app ID we don't have — skip link generation
return '';
} else if (typeName === 'timeline-item') {
const senderSlug = item.sender?.slug || '';
const senderType = item.sender?.typeName || targetParams.senderType || '';
if (senderSlug) {
if (senderType === 'workspace') return `${baseUrl}/workspaces/${senderSlug}/apps/timeline/timeline`;
if (senderType === 'page') return `${baseUrl}/pages/${senderSlug}/apps/timeline/timeline`;
}
}
return '';
}
function formatItem(baseUrl: string, item: any, index: number): string {
const targetParams = item.target?.params || {};
const title = (item.displayName || "Untitled").replace(/<\/?em>/g, '');
const link = buildLink(baseUrl, item);
let text = `[${index}] ${link ? `[${title}](${link})` : title}\n`;
text += ` ID: ${item.id}\n`;
text += ` Type: ${item.typeName || "Unknown"}\n`;
if (targetParams.appId) text += ` AppID: ${targetParams.appId}\n`;
if (targetParams.senderId || item.sender?.id) text += ` SenderID: ${targetParams.senderId || item.sender?.id}\n`;
text += ` Excerpt: ${item.excerpt || "—"}\n`;
text += ` Modified: ${item.modified || "—"}\n\n`;
return text;
}
export function registerSearchTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.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. SEARCH TIP: Use short, broad keyword queries (1-2 core terms) for best results. Avoid long phrases or full sentences — the API performs keyword matching, not semantic search.",
{
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 }: any): Promise<CallToolResult> => {
try {
// Strip surrounding quotes that LLMs sometimes add to search terms
const cleanQuery = query.replace(/^["']+|["']+$/g, '');
const params: Record<string, any> = { term: cleanQuery };
if (page !== undefined) params._page = page;
if (pageSize !== undefined) params._pageSize = pageSize;
const data = await client.request<any>({ method: "GET", path: "/api/search", params });
const items = data.content || [];
if (items.length === 0) {
return { content: [{ type: "text", text: `No results found for "${query}". Try a different search tool or rephrase the query.` }] };
}
let text = `Found ${items.length} results for "${query}":\n\n`;
const baseUrl = client.getBaseUrl().replace(/\/+$/, '');
items.forEach((item: any, i: number) => {
text += formatItem(baseUrl, item, i + 1);
});
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Search_QuickEntity",
"Quick entity search across DB Planet. Returns matching entities for fast lookups by name (e.g. people, workspaces, pages). Best for finding specific known entities. For broader topic-based search, use Search_FullText instead.",
{ query: z.string().describe("Quick search query") },
async ({ query }: any): Promise<CallToolResult> => {
try {
const cleanQuery = query.replace(/^["']+|["']+$/g, '');
const data = await client.request<any>({ method: "GET", path: "/api/quick-entity-search", params: { term: cleanQuery } });
const items = Array.isArray(data) ? data : (data?.content || data?.results || []);
if (items.length === 0) {
return { content: [{ type: "text", text: `No entities found for "${query}". Try Search_FullText for a broader content search.` }] };
}
const baseUrl = client.getBaseUrl().replace(/\/+$/, '');
let text = `Found ${items.length} entities for "${query}":\n\n`;
items.forEach((item: any, i: number) => {
text += formatItem(baseUrl, item, i + 1);
});
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Search_Grouped",
"Search DB Planet and return results grouped by content type (e.g. blog articles, wiki pages, workspaces).",
{ query: z.string().describe("Search query term") },
async ({ query }: any): Promise<CallToolResult> => {
try {
const cleanQuery = query.replace(/^["']+|["']+$/g, '');
const data = await client.request<any>({ method: "GET", path: "/api/search/grouped", params: { term: cleanQuery } });
const groups = data?.groups || {};
const totalElements = data?.totalElements || 0;
if (Object.keys(groups).length === 0) {
return { content: [{ type: "text", text: `No grouped results found for "${query}".` }] };
}
const baseUrl = client.getBaseUrl().replace(/\/+$/, '');
let text = `Found ${totalElements} results for "${query}" grouped by type:\n\n`;
let globalIndex = 1;
for (const [groupName, groupItems] of Object.entries(groups)) {
const items = groupItems as any[];
text += `--- ${groupName} (${items.length}) ---\n\n`;
items.forEach((item: any) => {
text += formatItem(baseUrl, item, globalIndex++);
});
}
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,93 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerSocialTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Social_GetComments",
"Get comments for a specific entity on DB Planet.",
{
targetId: z.string().describe("UUID of the entity (use Search_QuickEntity or Search_FullText to find it)"),
targetType: z.string().describe("Type name of the target. Only known valid types: blog-article, timeline-item, wiki-article. Other types like event are NOT supported."),
},
async ({ targetId, targetType }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: "/api/comments", params: { targetId, targetType } });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Social_PostComment",
"Post a comment on a specific entity on DB Planet.",
{
targetId: z.string().describe("UUID of the entity (use Search_QuickEntity or Search_FullText to find it)"),
targetType: z.string().describe("Type name of the target. Only known valid types: blog-article, timeline-item, wiki-article. Other types like event are NOT supported."),
body: z.string().describe("Comment text"),
},
async ({ targetId, targetType, body }: any): Promise<CallToolResult> => {
try {
const meData = await client.request<any>({ method: "GET", path: "/api/users/me" });
const data = await client.request<any>({
method: "POST",
path: "/api/comments",
data: {
authorId: meData.id,
targetId,
targetType,
message: body,
attachments: [],
fileLibraryAttachments: [],
},
});
return { content: [{ type: "text", text: "Comment posted successfully." }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Social_GetLikes",
"Get like count and whether the current user has liked a specific target.",
{
targetType: z.string().describe("Type of the like target. Only known valid types: blog-article, timeline-item, wiki-article. Other types like event are NOT supported."),
targetId: z.string().describe("UUID of the target (use Search_QuickEntity or Search_FullText to find it)"),
},
async ({ targetType, targetId }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/like-targets/${targetType}/${targetId}/likes` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Social_ToggleLike",
"Toggle a like on a specific target for a user.",
{
targetType: z.string().describe("Type of the like target. Only known valid types: blog-article, timeline-item, wiki-article. Other types like event are NOT supported."),
targetId: z.string().describe("UUID of the target (use Search_QuickEntity or Search_FullText to find it)"),
userId: z.string().describe("User UUID (use People_GetMe or People_Search to find it)"),
},
async ({ targetType, targetId, userId }: any): Promise<CallToolResult> => {
try {
await client.request<any>({ method: "POST", path: `/api/like-targets/${targetType}/${targetId}/likes/${userId}` });
return { content: [{ type: "text", text: "Like toggled successfully." }] };
} catch (error) {
return handleApiError(error);
}
}
);
}
@@ -0,0 +1,156 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { HaiiloClient } from "../haiiloClient.js";
import { handleApiError } from "../errorHandler.js";
export function registerWorkspaceTools(server: McpServer, client: HaiiloClient): void {
const s = server as any;
s.tool(
"Workspace_List",
"List workspaces on DB Planet. Use memberOnly=true to show only workspaces the current user is a member of (recommended for 'my workspaces' queries). Without the filter, returns one page of workspaces.",
{
memberOnly: z.boolean().optional().describe("If true, only return workspaces where the current user is a member or admin. Defaults to false."),
page: z.number().optional().describe("Page number (0-based). Defaults to 0."),
pageSize: z.number().optional().describe("Number of workspaces per page. Defaults to 20."),
},
async ({ memberOnly, page, pageSize }: any): Promise<CallToolResult> => {
try {
if (memberOnly) {
// Paginate through all workspaces to find the user's memberships
const allMyWorkspaces: any[] = [];
let currentPage = 0;
const batchSize = 100;
let hasMore = true;
while (hasMore) {
const data = await client.request<any>({
method: "GET",
path: "/api/workspaces",
params: { _page: currentPage, _pageSize: batchSize },
});
const items = data?.content || (Array.isArray(data) ? data : []);
for (const ws of items) {
if (ws.membershipStatus && ws.membershipStatus !== "NONE") {
allMyWorkspaces.push(ws);
}
}
hasMore = items.length === batchSize;
currentPage++;
}
if (allMyWorkspaces.length === 0) {
return { content: [{ type: "text", text: "You are not a member of any workspaces." }] };
}
let text = `You are a member of ${allMyWorkspaces.length} workspace(s):\n\n`;
allMyWorkspaces.forEach((ws: any, i: number) => {
text += `[${i + 1}] ${ws.name || ws.displayName || "Untitled"}\n`;
text += ` ID: ${ws.id}\n`;
text += ` Slug: ${ws.slug || "—"}\n`;
text += ` Role: ${ws.membershipStatus}\n`;
text += ` Visibility: ${ws.visibility || "—"}\n`;
text += ` Members: ${ws.memberCount || 0}\n`;
text += ` Description: ${(ws.description || "—").substring(0, 150)}\n\n`;
});
return { content: [{ type: "text", text }] };
}
// Default: return a single page
const params: Record<string, any> = {};
if (page !== undefined) params._page = page;
if (pageSize !== undefined) params._pageSize = pageSize;
const data = await client.request<any>({ method: "GET", path: "/api/workspaces", params });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Workspace_Get",
"Get workspace details by ID or slug. The id parameter must be a WORKSPACE UUID or slug, NOT a user ID. Use Workspace_List or Search_QuickEntity to find workspace IDs first.",
{ id: z.string().describe("Workspace UUID or slug (NOT a user ID)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/workspaces/${id}` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Workspace_GetMembers",
"Get members of a specific workspace. NOT for finding or listing workspaces — use Workspace_List or Search_QuickEntity first to find workspace IDs. The id parameter must be a WORKSPACE UUID, NOT a user ID.",
{ id: z.string().describe("Workspace UUID (NOT a user ID — use Workspace_List or Search_QuickEntity to find it)") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "GET", path: `/api/workspaces/${id}/members` });
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Workspace_Join",
"Join a workspace as the current user. Use Workspace_List or Search_QuickEntity to find the ID first.",
{ id: z.string().describe("Workspace UUID") },
async ({ id }: any): Promise<CallToolResult> => {
try {
const data = await client.request<any>({ method: "PUT", path: `/api/workspaces/${id}/users/join` });
return { content: [{ type: "text", text: `Successfully joined workspace ${id}.` }] };
} catch (error) {
return handleApiError(error);
}
}
);
s.tool(
"Workspace_Create",
"Create a new workspace on DB Planet.",
{
name: z.string().describe("Workspace name"),
visibility: z.string().optional().describe("Visibility: PUBLIC, CLOSED, or PRIVATE. Defaults to PUBLIC."),
},
async ({ name, visibility }: any): Promise<CallToolResult> => {
try {
const meData = await client.request<any>({ method: "GET", path: "/api/users/me" });
const data = await client.request<any>({
method: "POST",
path: "/api/workspaces",
data: {
id: null,
slug: null,
name,
description: "",
categoryIds: [],
adminIds: [meData.id],
adminGroupIds: [],
memberIds: [],
memberGroupIds: [],
visibility: visibility || "PUBLIC",
defaultLanguage: null,
translations: {},
archived: false,
},
});
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
return { content: [{ type: "text", text }] };
} catch (error) {
return handleApiError(error);
}
}
);
}