Skip to main content
Glama

Kibela MCP Server

MIT License
31
6
  • Apple
  • Linux
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js"; import { GraphQLClient } from "graphql-request"; import { FolderNotesResponse, GroupFoldersResponse, GroupNotesResponse, GroupResponse, LikeResponse, NoteContentResponse, NoteFromPathResponse, NotesResponse, RecentlyViewedNotesResponse, SearchResponse, UnlikeResponse, UsersResponse, } from "./types.js"; if (!process.env.KIBELA_TEAM || !process.env.KIBELA_TOKEN) { console.error("Required environment variables KIBELA_TEAM and KIBELA_TOKEN are not set"); process.exit(1); } const client = new GraphQLClient(`https://${process.env.KIBELA_TEAM}.kibe.la/api/v1`, { headers: { Authorization: `Bearer ${process.env.KIBELA_TOKEN}` }, }); const SEARCH_NOTES_TOOL: Tool = { name: "kibela_search_notes", description: "Search Kibela notes with given query", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, coediting: { type: "boolean", description: "Filter by co-editing status" }, isArchived: { type: "boolean", description: "Filter by archive status" }, sortBy: { type: "string", description: "Sort by (RELEVANT, CONTENT_UPDATED_AT)" }, userIds: { type: "array", items: { type: "string" }, description: "Filter by user IDs" }, folderIds: { type: "array", items: { type: "string" }, description: "Filter by folder IDs" }, }, required: ["query"], }, }; const GET_MY_NOTES_TOOL: Tool = { name: "kibela_get_my_notes", description: "Get your latest notes from Kibela", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Number of notes to fetch (max 50)", default: 15, }, }, }, }; const GET_NOTE_CONTENT_TOOL: Tool = { name: "kibela_get_note_content", description: "Get content and comments of a specific note", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note ID" }, include_image_data: { type: "boolean", description: "Whether to include image data URLs in the response", default: false, }, }, required: ["id"], }, }; const GET_GROUPS_TOOL: Tool = { name: "kibela_get_groups", description: "Get list of accessible groups", inputSchema: { type: "object", properties: {}, }, }; const GET_GROUP_FOLDERS_TOOL: Tool = { name: "kibela_get_group_folders", description: "Get folders in a group", inputSchema: { type: "object", properties: { groupId: { type: "string", description: "Group ID" }, parentFolderId: { type: "string", description: "Parent folder ID" }, }, required: ["groupId"], }, }; const GET_GROUP_NOTES_TOOL: Tool = { name: "kibela_get_group_notes", description: "Get notes in a group that are not attached to any folder", inputSchema: { type: "object", properties: { groupId: { type: "string", description: "Group ID" }, }, required: ["groupId"], }, }; const GET_FOLDER_NOTES_TOOL: Tool = { name: "kibela_get_folder_notes", description: "Get notes in a folder", inputSchema: { type: "object", properties: { folderId: { type: "string", description: "Folder ID" }, limit: { type: "number", description: "Number of notes to fetch (default 100)", default: 100, }, }, required: ["folderId"], }, }; const GET_USERS_TOOL: Tool = { name: "kibela_get_users", description: "Get list of users", inputSchema: { type: "object", properties: {}, }, }; const LIKE_NOTE_TOOL: Tool = { name: "kibela_like_note", description: "Like a note", inputSchema: { type: "object", properties: { noteId: { type: "string", description: "Note ID" }, }, required: ["noteId"], }, }; const UNLIKE_NOTE_TOOL: Tool = { name: "kibela_unlike_note", description: "Unlike a note", inputSchema: { type: "object", properties: { noteId: { type: "string", description: "Note ID" }, }, required: ["noteId"], }, }; const GET_RECENTLY_VIEWED_NOTES_TOOL: Tool = { name: "kibela_get_recently_viewed_notes", description: "Get your recently viewed notes", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Number of notes to fetch (max 15)", default: 15, }, }, }, }; const GET_NOTE_FROM_PATH_TOOL: Tool = { name: "kibela_get_note_from_path", description: "Get note content by its URL", inputSchema: { type: "object", properties: { path: { type: "string", description: "Note path (e.g. 'https://${process.env.KIBELA_TEAM}.kibe.la/notes/5154')" }, include_image_data: { type: "boolean", description: "Whether to include image data URLs in the response", default: false, }, }, required: ["path"], }, }; export const createServer = () => { const server = new Server( { name: "kibela-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ SEARCH_NOTES_TOOL, GET_MY_NOTES_TOOL, GET_NOTE_CONTENT_TOOL, GET_GROUPS_TOOL, GET_GROUP_FOLDERS_TOOL, GET_GROUP_NOTES_TOOL, GET_FOLDER_NOTES_TOOL, GET_USERS_TOOL, LIKE_NOTE_TOOL, UNLIKE_NOTE_TOOL, GET_RECENTLY_VIEWED_NOTES_TOOL, GET_NOTE_FROM_PATH_TOOL, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args = {} } = request.params; switch (name) { case "kibela_search_notes": { const { query, coediting, isArchived, sortBy, userIds, folderIds } = args as { query: string; coediting?: boolean; isArchived?: boolean; sortBy?: string; userIds?: string[]; folderIds?: string[]; }; const operation = ` query SearchNotes( $query: String!, $coediting: Boolean, $isArchived: Boolean, $sortBy: SearchSortKind, $userIds: [ID!], $folderIds: [ID!] ) { search( query: $query, first: 15, coediting: $coediting, isArchived: $isArchived, sortBy: $sortBy, userIds: $userIds, folderIds: $folderIds ) { edges { node { document { ... on Note { id title url contentUpdatedAt author { id account realName } groups { id name } } } } } } } `; const response = await client.request<SearchResponse>(operation, { query, coediting, isArchived, sortBy, userIds, folderIds, }); const notes = response.search.edges .filter((edge) => edge.node.document !== null) .map((edge) => edge.node.document); return { content: [ { type: "text", text: JSON.stringify(notes, null, 2), }, ], }; } case "kibela_get_my_notes": { const limit = (args.limit as number) || 15; const operation = ` query GetMyNotes($limit: Int!) { currentUser { latestNotes(first: $limit) { totalCount edges { node { id title url contentUpdatedAt author { id account realName } } } } } } `; const response = await client.request<NotesResponse>(operation, { limit }); const notes = response.currentUser.latestNotes.edges.map((edge) => edge.node); return { content: [ { type: "text", text: JSON.stringify(notes, null, 2), }, ], }; } case "kibela_get_note_content": { const id = args.id as string; const includeImageData = (args.include_image_data as boolean) || false; const attachmentsFragment = includeImageData ? `attachments(first: 3) { nodes { id name dataUrl mimeType } }` : ""; const operation = ` query GetNote($id: ID!) { note(id: $id) { id title content contentHtml contentUpdatedAt publishedAt url path isLikedByCurrentUser ${attachmentsFragment} author { id account realName } groups { id name } folders(first: 3) { nodes { id name fullName path } } comments(first: 3) { nodes { id content contentHtml author { account realName } createdAt } } } } `; const response = await client.request<NoteContentResponse>(operation, { id }); return { content: [ { type: "text", text: JSON.stringify(response.note, null, 2), }, ], }; } case "kibela_get_groups": { const operation = ` query GetGroups { groups(first: 10, ability: READABLE) { nodes { id name description isPrivate canBeManaged canBeJoinedBySelf isJoined } } } `; const response = await client.request<GroupResponse>(operation); return { content: [ { type: "text", text: JSON.stringify(response.groups.nodes, null, 2), }, ], }; } case "kibela_get_group_folders": { const { groupId, parentFolderId } = args as { groupId: string; parentFolderId?: string; }; const operation = ` query GetGroupFolders($groupId: ID!, $parentFolderId: ID) { group(id: $groupId) { folders(first: 30, active: true, parentFolderId: $parentFolderId) { nodes { id name fullName path canBeManaged parent { id name } notes(first: 10, active: true, orderBy: { field: CONTENT_UPDATED_AT, direction: DESC }) { nodes { id title contentUpdatedAt publishedAt author { account realName } } } } } } } `; const response = await client.request<GroupFoldersResponse>(operation, { groupId, parentFolderId, }); return { content: [ { type: "text", text: JSON.stringify(response.group.folders.nodes, null, 2), }, ], }; } case "kibela_get_group_notes": { const { groupId } = args as { groupId: string }; const operation = ` query GetGroupNotes($groupId: ID!) { group(id: $groupId) { notes(first: 10, active: true, onlyNotAttachedFolder: true, orderBy: { field: CONTENT_UPDATED_AT, direction: DESC }) { nodes { id title contentUpdatedAt publishedAt author { account realName } } } } } `; const response = await client.request<GroupNotesResponse>(operation, { groupId }); return { content: [ { type: "text", text: JSON.stringify(response.group.notes.nodes, null, 2), }, ], }; } case "kibela_get_folder_notes": { const { folderId, limit = 100 } = args as { folderId: string; limit?: number }; const operation = ` query GetFolderNotes($folderId: ID!, $limit: Int!) { folder(id: $folderId) { notes(first: $limit, active: true, orderBy: { field: CONTENT_UPDATED_AT, direction: DESC }) { nodes { id title contentUpdatedAt publishedAt author { account realName } } } } } `; const response = await client.request<FolderNotesResponse>(operation, { folderId, limit }); return { content: [ { type: "text", text: JSON.stringify(response.folder.notes.nodes, null, 2), }, ], }; } case "kibela_get_users": { const operation = ` query GetUsers { users(first: 100) { nodes { id account realName } } } `; const response = await client.request<UsersResponse>(operation); return { content: [ { type: "text", text: JSON.stringify(response.users.nodes, null, 2), }, ], }; } case "kibela_like_note": { const noteId = args.noteId as string; const operation = ` mutation LikeNote($input: LikeInput!) { like(input: $input) { clientMutationId likers(first: 3) { nodes { id account realName } } } } `; const response = await client.request<LikeResponse>(operation, { input: { noteId }, }); return { content: [ { type: "text", text: JSON.stringify(response.like, null, 2), }, ], }; } case "kibela_unlike_note": { const noteId = args.noteId as string; const operation = ` mutation UnlikeNote($input: UnlikeInput!) { unlike(input: $input) { clientMutationId likers(first: 10) { nodes { id account realName } } } } `; const response = await client.request<UnlikeResponse>(operation, { input: { noteId }, }); return { content: [ { type: "text", text: JSON.stringify(response.unlike, null, 2), }, ], }; } case "kibela_get_recently_viewed_notes": { const limit = (args.limit as number) || 15; const operation = ` query GetRecentlyViewedNotes($limit: Int!) { noteBrowsingHistories(first: $limit) { nodes { note { id title url contentUpdatedAt author { id account realName } } } } } `; const response = await client.request<RecentlyViewedNotesResponse>(operation, { limit }); const notes = response.noteBrowsingHistories.nodes.map((node) => node.note); return { content: [ { type: "text", text: JSON.stringify(notes, null, 2), }, ], }; } case "kibela_get_note_from_path": { const rawPath = args.path as string; const includeImageData = (args.include_image_data as boolean) || false; // Extract path from URL if full URL is provided const path = rawPath.includes("kibe.la/notes/") ? `/notes/${rawPath.split("/notes/")[1]}` : rawPath; const attachmentsFragment = includeImageData ? `attachments(first: 3) { nodes { id name dataUrl mimeType } }` : ""; const operation = ` query GetNoteFromPath($path: String!) { noteFromPath(path: $path) { id title content contentHtml contentUpdatedAt publishedAt url path isLikedByCurrentUser ${attachmentsFragment} author { id account realName } groups { id name } folders(first: 3) { nodes { id name fullName path } } comments(first: 3) { nodes { id content contentHtml author { account realName } createdAt } } } } `; const response = await client.request<NoteFromPathResponse>(operation, { path }); return { content: [ { type: "text", text: JSON.stringify(response.noteFromPath, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error("Error:", error); return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); return { server }; };

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/kiwamizamurai/mcp-kibela-server'

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