UNPKG

uservoice-mcp

Version:

Model Context Protocol (MCP) server for UserVoice feedback and suggestion management

2 lines 6.04 kB
#!/usr/bin/env node import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";const UV_API_BASE=process.env.UV_API_BASE,UV_TOKEN=process.env.UV_TOKEN;UV_API_BASE||(console.error("ERROR: UV_API_BASE environment variable is required"),process.exit(1)),UV_TOKEN||(console.error("ERROR: UV_TOKEN environment variable is required"),process.exit(1));const TOKEN=`Bearer ${UV_TOKEN}`,server=new McpServer({name:"uservoice-mcp",version:"1.2.0"});function parseTimePeriod(e){const r=[/(last|past)\s+(\d+)?\s*(days?|weeks?|months?|years?)/i,/from\s+the\s+(last|past)\s+(\d+)?\s*(days?|weeks?|months?|years?)/i,/in\s+the\s+(last|past)\s+(\d+)?\s*(days?|weeks?|months?|years?)/i];let t=null,o=null;for(const s of r)if(t=e.match(s),t){o=s;break}if(!t)return null;const[s,n,a,i]=t,c=a?parseInt(a):1,l=new Date;let u=new Date(l);switch(i.toLowerCase()){case"day":case"days":u.setDate(l.getDate()-c);break;case"week":case"weeks":u.setDate(l.getDate()-7*c);break;case"month":case"months":u.setMonth(l.getMonth()-c);break;case"year":case"years":u.setFullYear(l.getFullYear()-c)}let g=e.replace(s,"").trim();return g=g.replace(/\b(feedback|from|in|the|for|about)\b/gi,"").trim(),g=g.replace(/\s+/g," ").trim(),{startDate:u.toISOString(),period:s,cleanQuery:g}}async function makeUVRequest(e){const r={Authorization:TOKEN};try{console.error(`Making request to: ${e}`);const t=await fetch(e,{headers:r});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return await t.json()}catch(e){return console.error("Error making UV request:",e),null}}function formatSuggestion(e){return[`Title: ${e.title||"N/A"}`,`Status: ${e.status||"N/A"}`,`Votes: ${e.vote_count||0}`,`ID: ${e.id||"N/A"}`,`Created At: ${e.created_at||"N/A"}`,`Body: ${e.body||"N/A"}`,`URL: ${e.admin_url||"N/A"}`].join("\n")}async function main(){const e=new StdioServerTransport;await server.connect(e),console.error("UserVoice MCP Server running on stdio")}server.tool("get-feedback",{query:z.string().min(2).describe("Query string for feedback search. Use EXACT:query or 'exact query' or \"exact query\" format for precise search terms. Supports time periods like 'last week', 'past 3 days', 'last month'.")},(async({query:e})=>{console.error("\n=== QUERY PROCESSING DEBUG ==="),console.error(`Original query: "${e}"`),console.error(`Query length: ${e.length}`),console.error("Query character codes:",e.split("").map((e=>`${e}(${e.charCodeAt(0)})`)).join(" "));let r=null,t=e;if(console.error("\n--- EXACT QUERY DETECTION ---"),console.error(`Original query: "${e}"`),e.toLowerCase().startsWith("exact:")){const o=e.indexOf(":");r=e.substring(o+1).trim(),t="",console.error(`✅ EXACT: prefix detected. Extracted: "${r}"`)}else if(e.toLowerCase().startsWith("search:")){const o=e.indexOf(":");r=e.substring(o+1).trim(),t="",console.error(`✅ SEARCH: prefix detected. Extracted: "${r}"`)}else{const o=[{name:"Double brackets [[]]",regex:/\[\[(.*?)\]\]/},{name:'Double quotes ""',regex:/"(.*?)"/},{name:"Single quotes ''",regex:/'(.*?)'/},{name:"Backticks ``",regex:/`(.*?)`/}];for(const{name:s,regex:n}of o){const o=e.match(n);if(o){r=o[1].trim(),t=e.replace(n,"").trim(),console.error(`✅ ${s} detected. Extracted: "${r}"`);break}}}r||console.error("❌ No exact query delimiters found"),console.error("--- END EXACT QUERY DETECTION ---\n");const o=parseTimePeriod(t);let s=r||e,n="";if(console.error("\n--- SEARCH QUERY ASSIGNMENT ---"),console.error(`exactQuery: "${r}"`),console.error(`original query: "${e}"`),console.error(`searchQuery (before time processing): "${s}"`),console.error("Time period parsing result:",o),o){r?console.error(`Using exact query (ignoring time period cleaning): "${s}"`):(s=o.cleanQuery,console.error(`Using cleaned query from time parsing: "${s}"`));const e=(new Date).toISOString();n=`&period_start=${o.startDate}&period_end=${e}`,console.error(`Time filter: ${n}`)}console.error(`FINAL SEARCH QUERY: "${s}"`),console.error("--- END SEARCH QUERY ASSIGNMENT ---\n");const a=encodeURIComponent(s);console.error(`URL encoded query: "${a}"`),console.error("=== END DEBUG ===\n");const i=`${UV_API_BASE}/suggestions?q=${a}&sort=-created_at${n}`,c=await makeUVRequest(i);if(!c)return{content:[{type:"text",text:"Failed to retrieve suggestion data"}]};const l=c.suggestions||[],u=o?` from ${o.period}`:"";if(console.error(`Found ${l.length} suggestions for query: "${s}"${u}`),0===l.length)return{content:[{type:"text",text:`No active suggestions found for query: ${s}${u}`}]};return{content:[{type:"text",text:`Active suggestions for query: ${s}${u}\n\n${l.map(formatSuggestion).join("\n\n")}`}]}})),server.tool("debug-query-parsing",{testQuery:z.string().describe("Test query to debug the parsing logic")},(async({testQuery:e})=>{console.error("\n=== DEBUGGING QUERY PARSING ==="),console.error(`Test query: "${e}"`);const r=e.match(/\[\[(.*?)\]\]/);console.error("Regex match result:",r);let t=null,o=e;r&&(t=r[1],o=e.replace(/\[\[(.*?)\]\]/,"").trim());const s=parseTimePeriod(o),n={originalQuery:e,exactQueryMatch:r,extractedExactQuery:t,remainingAfterExact:o,timePeriodParsing:s,finalSearchQuery:t||(s?s.cleanQuery:e)};return console.error("Parsing result:",JSON.stringify(n,null,2)),{content:[{type:"text",text:`Query Parsing Debug Results:\n\n${JSON.stringify(n,null,2)}`}]}})),server.tool("test-connection",{message:z.string().optional().describe("Optional test message")},(async({message:e="test"})=>{const r={UV_API_BASE:UV_API_BASE?"✅ Set":"❌ Missing",UV_TOKEN:UV_TOKEN?"✅ Set":"❌ Missing",timestamp:(new Date).toISOString(),message:e};return console.error(`Test connection called with: ${JSON.stringify(r)}`),{content:[{type:"text",text:`UserVoice MCP Test Connection\n\nEnvironment Variables:\n- UV_API_BASE: ${r.UV_API_BASE}\n- UV_TOKEN: ${r.UV_TOKEN}\n\nTimestamp: ${r.timestamp}\nMessage: ${r.message}\n\nIf you see this, the MCP server is working!`}]}})),main().catch((e=>{console.error("Fatal error in main():",e),process.exit(1)}));