Skip to main content
Glama
smart.ts6.15 kB
/** * Smart Handlers * * AI-powered tool handlers for intelligent wiki operations */ import type { AppContext } from '../context.js'; import type { OutlineDocument } from '../types/api.js'; import type { WikiDocument } from '../brain/types.js'; import { ERROR_MESSAGES } from '../brain/constants.js'; export function createSmartHandlers({ apiClient, apiCall, config, brain }: AppContext) { const baseUrl = config.OUTLINE_URL; return { /** * Sync all documents to vector store for RAG * * Note: documents.list may return truncated or missing text, * so we fetch each document's full content via documents.info */ async sync_knowledge(args: { collectionId?: string }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } // Step 1: Fetch document list from Outline const payload: Record<string, unknown> = { limit: 100 }; if (args.collectionId) { payload.collectionId = args.collectionId; } const { data: docList } = await apiCall(() => apiClient.post<OutlineDocument[]>('/documents.list', payload) ); if (!docList || docList.length === 0) { return { message: ERROR_MESSAGES.NO_DOCUMENTS_FOUND, synced: 0 }; } // Step 2: Fetch full content for each document (list API may truncate text) const wikiDocs: WikiDocument[] = []; let fetchErrors = 0; for (const doc of docList) { try { const { data: fullDoc } = await apiCall(() => apiClient.post<OutlineDocument>('/documents.info', { id: doc.id }) ); if (fullDoc && fullDoc.text) { wikiDocs.push({ id: fullDoc.id, title: fullDoc.title, text: fullDoc.text, url: `${baseUrl}${fullDoc.url}`, collectionId: fullDoc.collectionId, }); } } catch { fetchErrors++; } } if (wikiDocs.length === 0) { return { message: ERROR_MESSAGES.NO_DOCUMENTS_WITH_CONTENT, synced: 0, errors: fetchErrors, }; } // Step 3: Sync to brain (vectorize) const result = await brain.syncDocuments(wikiDocs); return { message: `Successfully synced ${result.documents} documents (${result.chunks} chunks).`, documents: result.documents, chunks: result.chunks, skipped: docList.length - wikiDocs.length, errors: fetchErrors, }; }, /** * Ask a question and get an answer based on wiki content */ async ask_wiki(args: { question: string }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } const { answer, sources } = await brain.ask(args.question); return { answer, sources: sources.map((s) => ({ title: s.title, url: s.url, })), }; }, /** * Summarize a document */ async summarize_document(args: { documentId: string; language?: string }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } // Fetch document const { data } = await apiCall(() => apiClient.post<OutlineDocument>('/documents.info', { id: args.documentId }) ); if (!data.text) { return { error: ERROR_MESSAGES.NO_CONTENT_TO_SUMMARIZE }; } const summary = await brain.summarize(data.text, args.language); return { documentId: data.id, title: data.title, summary, }; }, /** * Suggest tags for a document */ async suggest_tags(args: { documentId: string }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } // Fetch document const { data } = await apiCall(() => apiClient.post<OutlineDocument>('/documents.info', { id: args.documentId }) ); if (!data.text) { return { error: ERROR_MESSAGES.NO_CONTENT_TO_ANALYZE }; } const tags = await brain.suggestTags(data.text); return { documentId: data.id, title: data.title, suggestedTags: tags, }; }, /** * Find documents related to a specific document */ async find_related(args: { documentId: string; limit?: number }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } // Fetch document const { data } = await apiCall(() => apiClient.post<OutlineDocument>('/documents.info', { id: args.documentId }) ); if (!data.text) { return { error: ERROR_MESSAGES.NO_CONTENT_TO_ANALYZE }; } // Search for similar documents const results = await brain.search(data.title + ' ' + data.text.substring(0, 500), args.limit || 5); // Filter out the source document const related = results.filter((r) => !r.id.startsWith(args.documentId)); return { documentId: data.id, title: data.title, related: related.map((r) => ({ title: r.title, url: r.url, excerpt: r.text.substring(0, 200) + '...', })), }; }, /** * Generate a Mermaid diagram from description */ async generate_diagram(args: { description: string }) { if (!brain.isEnabled()) { return { error: ERROR_MESSAGES.SMART_FEATURES_DISABLED }; } const diagram = await brain.generateDiagram(args.description); return { diagram, note: 'Copy this Mermaid code into an Outline document to render the diagram.', }; }, /** * Get brain status */ async smart_status() { const stats = await brain.getStats(); return { enabled: stats.enabled, indexedChunks: stats.chunks, message: stats.enabled ? `Smart features are enabled with ${stats.chunks} indexed chunks.` : ERROR_MESSAGES.SMART_FEATURES_DISABLED, }; }, }; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/huiseo/outline-smart-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server