Kibela MCP Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { GraphQLClient } from 'graphql-request';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { SearchResponse, NotesResponse, NoteContentResponse } 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"
}
},
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"
}
},
required: ["id"]
}
};
export const createServer = () => {
const server = new Server(
{
name: "kibela-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [SEARCH_NOTES_TOOL, GET_MY_NOTES_TOOL, GET_NOTE_CONTENT_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args = {} } = request.params;
switch (name) {
case "kibela_search_notes": {
const query = args.query as string;
if (!query) {
throw new Error("Query is required");
}
const operation = `
query SearchNotes($query: String!) {
search(query: $query, first: 15) {
edges {
node {
document {
... on Note {
id
title
url
}
}
}
}
}
}
`;
const response = await client.request<SearchResponse>(operation, { query });
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
}
}
}
}
}
`;
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;
if (!id) {
throw new Error("Note ID is required");
}
const operation = `
query GetNote($id: ID!) {
note(id: $id) {
contentHtml
comments(first:5) {
nodes {
content
author {
realName
}
}
}
}
}
`;
const response = await client.request<NoteContentResponse>(operation, { id });
return {
content: [{
type: "text",
text: JSON.stringify(response.note, 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 };
};