Skip to main content
Glama
index.ts9.34 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; import { fileURLToPath } from 'url'; const execAsync = promisify(exec); // Get the directory name of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const SCRIPTS_DIR = path.join(__dirname, '..', 'scripts'); class QuipServer { private server: Server; constructor() { this.server = new Server( { name: 'quip-document-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'quip_read_document', description: 'Read the content of a Quip document by its thread ID', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'The Quip document thread ID' } }, required: ['threadId'], }, }, { name: 'quip_append_content', description: 'Append content to an existing Quip document', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'The Quip document thread ID' }, content: { type: 'string', description: 'Markdown content to append to the document' } }, required: ['threadId', 'content'], }, }, { name: 'quip_prepend_content', description: 'Add content to the beginning of an existing Quip document', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'The Quip document thread ID' }, content: { type: 'string', description: 'Markdown content to prepend to the document' } }, required: ['threadId', 'content'], }, }, { name: 'quip_replace_content', description: 'Replace content in an existing Quip document', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'The Quip document thread ID' }, content: { type: 'string', description: 'New markdown content to replace the document content' } }, required: ['threadId', 'content'], }, }, { name: 'quip_create_document', description: 'Create a new Quip document', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the new document' }, content: { type: 'string', description: 'Initial markdown content for the document' } }, required: ['title', 'content'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (!args) { throw new McpError( ErrorCode.InvalidParams, 'Arguments are required' ); } switch (name) { case 'quip_read_document': { const typedArgs = args as any; if (!typedArgs.threadId) { throw new McpError(ErrorCode.InvalidParams, 'threadId is required'); } return await this.readDocument(String(typedArgs.threadId)); } case 'quip_append_content': { const typedArgs = args as any; if (!typedArgs.threadId || !typedArgs.content) { throw new McpError(ErrorCode.InvalidParams, 'threadId and content are required'); } return await this.editDocument(String(typedArgs.threadId), String(typedArgs.content), 'APPEND'); } case 'quip_prepend_content': { const typedArgs = args as any; if (!typedArgs.threadId || !typedArgs.content) { throw new McpError(ErrorCode.InvalidParams, 'threadId and content are required'); } return await this.editDocument(String(typedArgs.threadId), String(typedArgs.content), 'PREPEND'); } case 'quip_replace_content': { const typedArgs = args as any; if (!typedArgs.threadId || !typedArgs.content) { throw new McpError(ErrorCode.InvalidParams, 'threadId and content are required'); } return await this.editDocument(String(typedArgs.threadId), String(typedArgs.content), 'REPLACE'); } case 'quip_create_document': { const typedArgs = args as any; if (!typedArgs.title || !typedArgs.content) { throw new McpError(ErrorCode.InvalidParams, 'title and content are required'); } return await this.createDocument(String(typedArgs.title), String(typedArgs.content)); } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { console.error('Error:', error); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } private async readDocument(threadId: string) { try { console.log(`Reading document ${threadId}...`); // Execute the Python script to read the document const command = `python -u ${path.join(SCRIPTS_DIR, 'quip_edit.py')} ${threadId} read`; const { stdout, stderr } = await execAsync(command); if (stderr) { console.error(`Error reading document: ${stderr}`); throw new Error(stderr); } return { content: [ { type: 'text', text: stdout || 'Document read successfully, but no content was returned', }, ], }; } catch (error) { console.error('Error reading document:', error); throw error; } } private async editDocument(threadId: string, content: string, operation: string) { try { console.log(`Editing document ${threadId} with operation ${operation}...`); // Create a temporary file to store the content const tempFilePath = `/tmp/quip_content_${Date.now()}.md`; const writeCommand = `echo "${content.replace(/"/g, '\\"')}" > ${tempFilePath}`; await execAsync(writeCommand); // Execute the Python script to edit the document const command = `python -u ${path.join(SCRIPTS_DIR, 'quip_edit.py')} ${threadId} ${operation.toLowerCase()} ${tempFilePath}`; const { stdout, stderr } = await execAsync(command); // Clean up the temporary file await execAsync(`rm ${tempFilePath}`); if (stderr) { console.error(`Error editing document: ${stderr}`); throw new Error(stderr); } return { content: [ { type: 'text', text: stdout || `Successfully ${operation.toLowerCase()}ed content to document ${threadId}`, }, ], }; } catch (error) { console.error(`Error ${operation.toLowerCase()}ing document:`, error); throw error; } } private async createDocument(title: string, content: string) { try { console.log(`Creating document "${title}"...`); // Not implemented in the Python script yet return { content: [ { type: 'text', text: `Document creation is not implemented in the current Python script. Please use the Quip web interface to create new documents.`, }, ], }; } catch (error) { console.error('Error creating document:', error); throw error; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Quip MCP server running on stdio'); } } const server = new QuipServer(); server.run().catch(console.error);

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/AvinashBole/quip-mcp-server'

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