Skip to main content
Glama
index.ts13 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { Hono } from "hono"; import { z } from "zod"; // Zod schemas for validation const SearchItemsSchema = z.object({ query: z.string().optional(), page: z.number().min(1).max(100).optional().default(1), per_page: z.number().min(1).max(100).optional().default(20), }); const GetItemSchema = z.object({ item_id: z.string(), }); const GetItemsByTagSchema = z.object({ tag_id: z.string(), page: z.number().min(1).max(100).optional().default(1), per_page: z.number().min(1).max(100).optional().default(20), }); const GetItemsByUserSchema = z.object({ user_id: z.string(), page: z.number().min(1).max(100).optional().default(1), per_page: z.number().min(1).max(100).optional().default(20), }); const GetTagsSchema = z.object({ page: z.number().min(1).max(100).optional().default(1), per_page: z.number().min(1).max(100).optional().default(20), sort: z.enum(["count", "name"]).optional().default("count"), }); const GetItemCommentsSchema = z.object({ item_id: z.string(), }); const GetUserStocksSchema = z.object({ user_id: z.string(), page: z.number().min(1).max(100).optional().default(1), per_page: z.number().min(1).max(100).optional().default(20), }); // Type definitions type SearchItemsParams = z.infer<typeof SearchItemsSchema>; type GetItemParams = z.infer<typeof GetItemSchema>; type GetItemsByTagParams = z.infer<typeof GetItemsByTagSchema>; type GetItemsByUserParams = z.infer<typeof GetItemsByUserSchema>; type GetTagsParams = z.infer<typeof GetTagsSchema>; type GetItemCommentsParams = z.infer<typeof GetItemCommentsSchema>; type GetUserStocksParams = z.infer<typeof GetUserStocksSchema>; // Qiita API Client class QiitaClient { private baseUrl: string; private accessToken: string | undefined; constructor() { const teamName = process.env.QIITA_TEAM; this.baseUrl = teamName ? `https://${teamName}.qiita.com/api/v2` : "https://qiita.com/api/v2"; this.accessToken = process.env.QIITA_ACCESS_TOKEN; } private async fetch(endpoint: string): Promise<any> { const headers: HeadersInit = { "Content-Type": "application/json", }; if (this.accessToken) { headers["Authorization"] = `Bearer ${this.accessToken}`; } const response = await fetch(`${this.baseUrl}${endpoint}`, { headers }); if (!response.ok) { const errorBody = await response.text(); throw new Error( `Qiita API error: ${response.status} ${response.statusText} - ${errorBody}` ); } return response.json(); } async searchItems(params: SearchItemsParams): Promise<any[]> { const validated = SearchItemsSchema.parse(params); const urlParams = new URLSearchParams({ page: validated.page.toString(), per_page: validated.per_page.toString(), }); if (validated.query) { urlParams.append("query", validated.query); } return this.fetch(`/items?${urlParams.toString()}`); } async getItem(params: GetItemParams): Promise<any> { const validated = GetItemSchema.parse(params); return this.fetch(`/items/${validated.item_id}`); } async getItemsByTag(params: GetItemsByTagParams): Promise<any[]> { const validated = GetItemsByTagSchema.parse(params); const urlParams = new URLSearchParams({ page: validated.page.toString(), per_page: validated.per_page.toString(), }); return this.fetch(`/tags/${validated.tag_id}/items?${urlParams.toString()}`); } async getItemsByUser(params: GetItemsByUserParams): Promise<any[]> { const validated = GetItemsByUserSchema.parse(params); const urlParams = new URLSearchParams({ page: validated.page.toString(), per_page: validated.per_page.toString(), }); return this.fetch(`/users/${validated.user_id}/items?${urlParams.toString()}`); } async getTags(params: GetTagsParams): Promise<any[]> { const validated = GetTagsSchema.parse(params); const urlParams = new URLSearchParams({ page: validated.page.toString(), per_page: validated.per_page.toString(), sort: validated.sort, }); return this.fetch(`/tags?${urlParams.toString()}`); } async getItemComments(params: GetItemCommentsParams): Promise<any[]> { const validated = GetItemCommentsSchema.parse(params); return this.fetch(`/items/${validated.item_id}/comments`); } async getUserStocks(params: GetUserStocksParams): Promise<any[]> { const validated = GetUserStocksSchema.parse(params); const urlParams = new URLSearchParams({ page: validated.page.toString(), per_page: validated.per_page.toString(), }); return this.fetch(`/users/${validated.user_id}/stocks?${urlParams.toString()}`); } } // Tool definitions const tools: Tool[] = [ { name: "search_items", description: "Search Qiita articles. You can search with a query string or get recent articles without a query.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query (optional)", }, page: { type: "number", description: "Page number (1-100, default: 1)", minimum: 1, maximum: 100, }, per_page: { type: "number", description: "Items per page (1-100, default: 20)", minimum: 1, maximum: 100, }, }, }, }, { name: "get_item", description: "Get a specific Qiita article by its ID.", inputSchema: { type: "object", properties: { item_id: { type: "string", description: "Article ID", }, }, required: ["item_id"], }, }, { name: "get_items_by_tag", description: "Get Qiita articles with a specific tag.", inputSchema: { type: "object", properties: { tag_id: { type: "string", description: "Tag ID (e.g., 'Python', 'JavaScript')", }, page: { type: "number", description: "Page number (1-100, default: 1)", minimum: 1, maximum: 100, }, per_page: { type: "number", description: "Items per page (1-100, default: 20)", minimum: 1, maximum: 100, }, }, required: ["tag_id"], }, }, { name: "get_items_by_user", description: "Get articles written by a specific user.", inputSchema: { type: "object", properties: { user_id: { type: "string", description: "User ID", }, page: { type: "number", description: "Page number (1-100, default: 1)", minimum: 1, maximum: 100, }, per_page: { type: "number", description: "Items per page (1-100, default: 20)", minimum: 1, maximum: 100, }, }, required: ["user_id"], }, }, { name: "get_tags", description: "Get a list of tags used in Qiita.", inputSchema: { type: "object", properties: { page: { type: "number", description: "Page number (1-100, default: 1)", minimum: 1, maximum: 100, }, per_page: { type: "number", description: "Items per page (1-100, default: 20)", minimum: 1, maximum: 100, }, sort: { type: "string", description: "Sort order: 'count' (by item count) or 'name' (default: 'count')", enum: ["count", "name"], }, }, }, }, { name: "get_item_comments", description: "Get comments on a specific article.", inputSchema: { type: "object", properties: { item_id: { type: "string", description: "Article ID", }, }, required: ["item_id"], }, }, { name: "get_user_stocks", description: "Get articles that a user has stocked (bookmarked).", inputSchema: { type: "object", properties: { user_id: { type: "string", description: "User ID", }, page: { type: "number", description: "Page number (1-100, default: 1)", minimum: 1, maximum: 100, }, per_page: { type: "number", description: "Items per page (1-100, default: 20)", minimum: 1, maximum: 100, }, }, required: ["user_id"], }, }, ]; // Create server instance const server = new Server( { name: "qiita-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Initialize Qiita client const qiitaClient = new QiitaClient(); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Call tool handler with Zod validation server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "search_items": { const result = await qiitaClient.searchItems((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_item": { const result = await qiitaClient.getItem((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_items_by_tag": { const result = await qiitaClient.getItemsByTag((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_items_by_user": { const result = await qiitaClient.getItemsByUser((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_tags": { const result = await qiitaClient.getTags((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_item_comments": { const result = await qiitaClient.getItemComments((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_user_stocks": { const result = await qiitaClient.getUserStocks((args || {}) as any); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); // Start server with Hono and SSE support async function main() { const app = new Hono(); const port = Number(process.env.PORT) || 3000; const { createServer } = await import("http"); // Health check endpoint app.get("/health", (c) => { return c.json({ status: "ok", timestamp: new Date().toISOString() }); }); // Message endpoint (POST) app.post("/message", async (c) => { return c.text("OK", 200); }); // Create HTTP server that handles both SSE and Hono routes const httpServer = createServer(async (req, res) => { // Handle SSE endpoint if (req.url === "/sse" && req.method === "GET") { console.error("Client connecting via SSE..."); const transport = new SSEServerTransport("/message", res); await server.connect(transport); console.error("Client connected via SSE"); return; } // Handle other routes with Hono const request = new Request(`http://localhost:${port}${req.url}`, { method: req.method, headers: req.headers as any, }); const response = await app.fetch(request); res.statusCode = response.status; response.headers.forEach((value, key) => { res.setHeader(key, value); }); if (response.body) { const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; res.write(value); } } res.end(); }); httpServer.listen(port, () => { console.error(`Qiita MCP Server running on http://localhost:${port}`); console.error(`SSE endpoint: http://localhost:${port}/sse`); console.error(`Health check: http://localhost:${port}/health`); }); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });

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/ningen/qiita-mcp-server'

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