byterover-mcp
Version:
Model Context Protocol server for ByteRover - a sharing memory layer for your vibe coding agent
7 lines (5 loc) • 21.3 kB
JavaScript
import{StdioServerTransport as j}from"@modelcontextprotocol/sdk/server/stdio.js";import k from"yargs";import{hideBin as $}from"yargs/helpers";function A(c){return c.length<=4?"****":`****${c.slice(-4)}`}function I(c){let e=k($(process.argv)).options({"byterover-public-api-key":{type:"string",description:"Byterover public API key"},port:{type:"number",description:"Port to run the server on"},"user-id":{type:"string",description:"User ID to run the server on"},"http-timeout":{type:"number",description:"HTTP request timeout in milliseconds (default: 30000)"},"max-retries":{type:"number",description:"Maximum number of retries for failed requests (default: 3)"},"max-concurrent-requests":{type:"number",description:"Maximum number of concurrent requests (default: 10)"},"health-check-interval":{type:"number",description:"Health check interval in milliseconds (default: 30000)"}}).help().version("0.2.2").parseSync(),t={byteroverPublicApiKey:"",port:3333,userId:"",configSources:{byteroverPublicApiKey:"env",port:"default",userId:"env"},isConfigured:!1,httpTimeout:e["http-timeout"]||parseInt(process.env.BYTEROVER_HTTP_TIMEOUT||"30000",10),maxRetries:e["max-retries"]||parseInt(process.env.BYTEROVER_MAX_RETRIES||"3",10),maxConcurrentRequests:e["max-concurrent-requests"]||parseInt(process.env.BYTEROVER_MAX_CONCURRENT||"10",10),healthCheckInterval:e["health-check-interval"]||parseInt(process.env.BYTEROVER_HEALTH_CHECK_INTERVAL||"30000",10)};return e["byterover-public-api-key"]&&(t.byteroverPublicApiKey=e["byterover-public-api-key"],t.configSources.byteroverPublicApiKey="cli"),e.port?(t.port=e.port,t.configSources.port="cli"):process.env.PORT&&(t.port=parseInt(process.env.PORT,10),t.configSources.port="env"),e["user-id"]&&(t.userId=e["user-id"],t.configSources.userId="cli"),t.isConfigured=!!(t.byteroverPublicApiKey&&t.userId),t.byteroverPublicApiKey||(t.configSources.byteroverPublicApiKey="none",console.warn("Byterover public API key is not provided. Server will start with limited functionality."),console.warn("Use the /config endpoint to set the API key after server startup.")),t.userId||(t.configSources.userId="none",console.warn("User ID is not provided. Server will start with limited functionality."),console.warn("Use the /config endpoint to set the user ID after server startup.")),c||(console.log(`
Configuration:`),console.log(` Byterover Public API Key: ${t.byteroverPublicApiKey?A(t.byteroverPublicApiKey):"Not configured"}`),console.log(` Port: ${t.port}`),console.log(` User ID: ${t.userId||"Not configured"}`),console.log(` Server Status: ${t.isConfigured?"Fully configured":"Limited functionality"}`),console.log(`
Advanced Configuration:`),console.log(` HTTP Timeout: ${t.httpTimeout}ms`),console.log(` Max Retries: ${t.maxRetries}`),console.log(` Max Concurrent Requests: ${t.maxConcurrentRequests}`),console.log(` Health Check Interval: ${t.healthCheckInterval}ms`)),t}import{randomUUID as H}from"node:crypto";import S from"express";import{SSEServerTransport as M}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as B}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as O}from"@modelcontextprotocol/sdk/types.js";var r={isHTTP:!1,log:(...c)=>{r.isHTTP?console.log("[INFO]",...c):console.error("[INFO]",...c)},warn:(...c)=>{console.warn("[WARN]",...c)},error:(...c)=>{console.error("[ERROR]",...c)}};var E=25e3,N=null,m={streamable:{},sse:{}},d=new Map;async function w(c,e){let t=S();t.use("/mcp",S.json()),t.use((i,s,u)=>{r.log(`${i.method} ${i.path} - Headers: ${JSON.stringify(i.headers)}`),u()});let o=S.Router();o.use(S.json()),o.post("/",(i,s)=>{let{byteroverPublicApiKey:u,userId:n}=i.body;if(!u||!n)return s.status(400).json({success:!1,message:"Both byteroverPublicApiKey and userId are required"});try{return e.updateCredentials(u,n)?(r.log("Byterover credentials updated successfully"),s.status(200).json({success:!0,message:"Byterover credentials updated successfully"})):(r.error("Failed to update Byterover credentials"),s.status(400).json({success:!1,message:"Failed to update Byterover credentials"}))}catch(l){let h=l instanceof Error?l.message:String(l);return r.error("Error updating Byterover credentials:",h),s.status(500).json({success:!1,message:"Error updating Byterover credentials",error:h})}}),o.get("/status",(i,s)=>{let u=e.isServiceConfigured();return s.status(200).json({success:!0,isConfigured:u,message:u?"Server is fully configured":"Server is running with limited functionality"})}),t.use("/config",o),t.post("/mcp",async(i,s)=>{r.log("Received StreamableHTTP request");let u=i.headers["mcp-session-id"],n;if(u&&m.streamable[u])r.log("Reusing existing StreamableHTTP transport for sessionId",u),n=m.streamable[u];else if(!u&&O(i.body))r.log("New initialization request for StreamableHTTP sessionId",u),n=new B({sessionIdGenerator:()=>H(),onsessioninitialized:g=>{m.streamable[g]=n}}),n.onclose=()=>{n.sessionId&&delete m.streamable[n.sessionId]},await e.connect(n);else{r.log("Invalid request:",i.body),s.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}let l=null,h=i.body.params?._meta?.progressToken,p=0;h&&(r.log(`Setting up progress notifications for token ${h} on session ${u}`),l=setInterval(async()=>{r.log("Sending progress notification",p),await e.server.notification({method:"notifications/progress",params:{progress:p,progressToken:h}}),p++},1e3)),r.log("Handling StreamableHTTP request"),await n.handleRequest(i,s,i.body),l&&clearInterval(l),r.log("StreamableHTTP request handled")});let a=async(i,s)=>{let u=i.headers["mcp-session-id"];if(!u||!m.streamable[u]){s.status(400).send("Invalid or missing session ID");return}console.log(`Received session termination request for session ${u}`);try{await m.streamable[u].handleRequest(i,s)}catch(n){console.error("Error handling session termination:",n),s.headersSent||s.status(500).send("Error processing session termination")}};t.get("/mcp",a),t.delete("/mcp",a),t.get("/sse",async(i,s)=>{r.log("Establishing new SSE connection");let u=new M("/messages",s),n=u.sessionId;r.log(`New SSE connection established for sessionId ${n}`),r.log("/sse request headers:",i.headers),r.log("/sse request body:",i.body),m.sse[n]=u;let l=setInterval(()=>{let h=d.get(n);if(h&&!h.res.writableEnded)try{h.res.write(`: keepalive
`),r.log(`Keep-alive sent for SSE session ${n}`)}catch(p){let g=p instanceof Error?p.message:String(p);r.error(`Failed to send keep-alive for session ${n}:`,g),clearInterval(l),d.delete(n),delete m.sse[n]}else r.log(`SSE connection ${n} no longer writable, cleaning up keep-alive`),clearInterval(l),d.delete(n)},E);d.set(n,{res:s,intervalId:l,transport:u}),r.log(`SSE connection ${n} started with keep-alive interval`),s.on("close",()=>{r.log(`SSE connection ${n} closed, cleaning up resources`),delete m.sse[n];let h=d.get(n);h&&(clearInterval(h.intervalId),d.delete(n))}),s.on("error",h=>{let p=h instanceof Error?h.message:String(h);r.error(`SSE connection ${n} error:`,p),delete m.sse[n];let g=d.get(n);g&&(clearInterval(g.intervalId),d.delete(n))});try{await e.connect(u),r.log(`MCP server connected to SSE transport ${n}`)}catch(h){let p=h instanceof Error?h.message:String(h);r.error(`Failed to connect MCP server to SSE transport ${n}:`,p),clearInterval(l),d.delete(n),delete m.sse[n],s.writableEnded||s.status(500).end("Failed to connect MCP server to transport")}}),t.post("/sse",async(i,s)=>{r.log("Received POST request to /sse endpoint - SSE requires GET"),s.status(405).json({error:"Method Not Allowed",message:"SSE endpoint requires GET request. Use GET /sse to establish SSE connection."})}),t.post("/messages",async(i,s)=>{let u=i.query.sessionId,n=m.sse[u];if(n)r.log(`Received SSE message for sessionId ${u}`),r.log("/messages request headers:",i.headers),r.log("/messages request body:",i.body),await n.handlePostMessage(i,s);else{r.error(`No transport found for sessionId ${u} - session may have expired`),s.status(400).json({error:"Session not found",message:`No active SSE transport found for sessionId ${u}. The session may have expired or been terminated.`,code:-32e3});return}}),N=t.listen(c,()=>{r.log(`HTTP server listening on port ${c}`),r.log(`SSE endpoint available at http://localhost:${c}/sse`),r.log(`Message endpoint available at http://localhost:${c}/messages`),r.log(`StreamableHTTP endpoint available at http://localhost:${c}/mcp`),r.log(`SSE keep-alive interval set to ${E}ms`)}),process.on("SIGINT",async()=>{r.log("Shutting down server...");for(let[i,s]of d.entries())clearInterval(s.intervalId),r.log(`Cleared keep-alive interval for session ${i}`);d.clear(),await q(m.sse),await q(m.streamable),r.log("Server shutdown complete"),process.exit(0)})}async function q(c){for(let e in c)try{let t=d.get(e);t&&(clearInterval(t.intervalId),d.delete(e)),await c[e].close(),delete c[e]}catch(t){console.error(`Error closing transport for session ${e}:`,t)}}import{McpServer as K}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as y}from"zod";var f=class extends Error{constructor(t,o,a,i){super(`HTTP ${t}: ${o} at ${a}`);this.status=t;this.statusText=o;this.url=a;this.body=i;this.name="HttpError"}},C=class extends Error{constructor(t,o){super(`Request to ${t} timed out after ${o}ms`);this.url=t;this.timeout=o;this.name="TimeoutError"}},b=class extends Error{constructor(t,o){super(`Network error accessing ${t}: ${o.message}`);this.url=t;this.originalError=o;this.name="NetworkError"}},v=class{constructor(e={}){this.config={timeout:e.timeout??3e4,maxRetries:e.maxRetries??3,retryDelay:e.retryDelay??1e3,keepAlive:e.keepAlive??!0}}async request(e,t={}){let{timeout:o=this.config.timeout,maxRetries:a=this.config.maxRetries,retryDelay:i=this.config.retryDelay,...s}=t,u=null;for(let n=0;n<=a;n++)try{let l=await this.fetchWithTimeout(e,s,o);if(!l.ok){let h=await l.text().catch(()=>null);throw new f(l.status,l.statusText,e,h)}return await l.json()}catch(l){if(u=l,l instanceof f&&l.status>=400&&l.status<500||n===a)throw l;if(n<a){let h=i*Math.pow(2,n);r.warn(`Request failed (attempt ${n+1}/${a+1}), retrying in ${h}ms:`,l instanceof Error?l.message:String(l)),await this.sleep(h)}}throw u||new Error("Request failed after all retries")}async fetchWithTimeout(e,t,o){let a=new AbortController,i=setTimeout(()=>a.abort(),o);try{return await fetch(e,{...t,signal:a.signal})}catch(s){throw s instanceof Error?s.name==="AbortError"?new C(e,o):new b(e,s):s}finally{clearTimeout(i)}}sleep(e){return new Promise(t=>setTimeout(t,e))}updateConfig(e){this.config={...this.config,...e},r.log("HttpClient configuration updated:",this.config)}};var T=class{constructor(e,t={}){this.state="disconnected";this.consecutiveFailures=0;this.listeners=[];this.healthCheckUrl=e,this.config={healthCheckInterval:t.healthCheckInterval??3e4,healthCheckTimeout:t.healthCheckTimeout??5e3,maxConsecutiveFailures:t.maxConsecutiveFailures??3,authHeaders:t.authHeaders??{}},this.authHeaders=this.config.authHeaders,this.httpClient=new v({timeout:this.config.healthCheckTimeout,maxRetries:0})}start(){this.healthCheckTimer||(r.log("Starting connection monitor"),this.performHealthCheck(),this.healthCheckTimer=setInterval(()=>this.performHealthCheck(),this.config.healthCheckInterval))}stop(){this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=void 0),this.setState("disconnected"),r.log("Stopped connection monitor")}getState(){return this.state}onStateChange(e){return this.listeners.push(e),()=>{let t=this.listeners.indexOf(e);t>-1&&this.listeners.splice(t,1)}}async performHealthCheck(){try{await this.httpClient.request(this.healthCheckUrl,{method:"GET",headers:this.authHeaders}),this.consecutiveFailures=0,this.state!=="connected"&&(this.setState("connected"),r.log("Connection restored"))}catch(e){this.consecutiveFailures++,r.warn(`Health check failed (${this.consecutiveFailures}/${this.config.maxConsecutiveFailures}):`,e instanceof Error?e.message:String(e)),this.consecutiveFailures>=this.config.maxConsecutiveFailures&&(this.state==="connected"?this.setState("reconnecting"):this.state==="reconnecting"&&this.setState("error",e))}}setState(e,t){if(this.state===e)return;let o=this.state;this.state=e;let a={previousState:o,currentState:e,error:t};this.listeners.forEach(i=>{try{i(a)}catch(s){r.error("Error in connection state change listener:",s instanceof Error?s.message:String(s))}})}async checkNow(){return await this.performHealthCheck(),this.state==="connected"}updateAuthHeaders(e){this.authHeaders=e,this.config.authHeaders=e}};var x=class{constructor(e={}){this.queue=[];this.activeRequests=0;this.requestId=0;this.config={maxConcurrent:e.maxConcurrent??5,maxQueueSize:e.maxQueueSize??50,requestTimeout:e.requestTimeout??6e4}}async enqueue(e){return new Promise((t,o)=>{let a=String(++this.requestId);if(this.queue.length>=this.config.maxQueueSize){o(new Error(`Request queue is full (max: ${this.config.maxQueueSize})`));return}let i={id:a,execute:e,resolve:t,reject:o,timestamp:Date.now()};this.queue.push(i),r.log(`Request ${a} queued. Queue size: ${this.queue.length}`),this.processQueue()})}async processQueue(){if(this.activeRequests>=this.config.maxConcurrent||this.queue.length===0)return;let e=this.queue.shift();if(!e)return;let t=Date.now()-e.timestamp;if(t>this.config.requestTimeout){e.reject(new Error(`Request timed out after ${t}ms in queue`)),this.processQueue();return}this.activeRequests++,r.log(`Processing request ${e.id}. Active: ${this.activeRequests}/${this.config.maxConcurrent}`);try{let o=await e.execute();e.resolve(o)}catch(o){e.reject(o)}finally{this.activeRequests--,r.log(`Request ${e.id} completed. Active: ${this.activeRequests}/${this.config.maxConcurrent}`),this.processQueue()}}getStats(){return{queueLength:this.queue.length,activeRequests:this.activeRequests,maxConcurrent:this.config.maxConcurrent,maxQueueSize:this.config.maxQueueSize}}clear(){let e=new Error("Queue cleared");for(;this.queue.length>0;)this.queue.shift()?.reject(e);r.log("Request queue cleared")}};var R=class{constructor(e="",t="",o={}){this.baseUrl="https://api.byterover.dev/api/v1";this.byteroverPublicApiKey=e,this.userId=t,this.isConfigured=!!(e&&t),this.config={httpTimeout:o.httpTimeout??3e4,maxRetries:o.maxRetries??3,maxConcurrentRequests:o.maxConcurrentRequests??5,healthCheckInterval:o.healthCheckInterval??3e4},this.httpClient=new v({timeout:this.config.httpTimeout,maxRetries:this.config.maxRetries,retryDelay:1e3,keepAlive:!0}),this.requestQueue=new x({maxConcurrent:this.config.maxConcurrentRequests,maxQueueSize:50,requestTimeout:6e4}),this.isConfigured&&this.initializeConnectionMonitor()}initializeConnectionMonitor(){let e=`${this.baseUrl.replace("/api/v1","/api")}/health`;this.connectionMonitor=new T(e,{healthCheckInterval:this.config.healthCheckInterval,healthCheckTimeout:5e3,maxConsecutiveFailures:3,authHeaders:{"x-api-key":this.byteroverPublicApiKey}}),this.connectionMonitor.onStateChange(t=>{r.log(`Connection state changed: ${t.previousState} -> ${t.currentState}`),t.error&&r.error("Connection error:",t.error.message)}),this.connectionMonitor.start()}updateCredentials(e,t){return!e||!t?!1:(this.byteroverPublicApiKey=e,this.userId=t,this.isConfigured=!0,this.connectionMonitor?this.connectionMonitor.updateAuthHeaders({"x-api-key":this.byteroverPublicApiKey}):this.initializeConnectionMonitor(),r.log("ByteroverService credentials updated"),!0)}isServiceConfigured(){return this.isConfigured}getConnectionState(){return this.connectionMonitor?.getState()??"disconnected"}async healthCheck(){if(!this.isConfigured)return!1;try{return await this.httpClient.request(`${this.baseUrl.replace("/api/v1","/api")}/health`,{method:"GET",headers:{"x-api-key":this.byteroverPublicApiKey},timeout:5e3}),!0}catch(e){return r.error("Health check failed:",e instanceof Error?e.message:String(e)),!1}}async request(e,t={}){if(!this.isConfigured)throw new Error("ByteroverService is not configured with valid credentials. Please set the API key and user ID first.");if(typeof fetch!="function")throw new Error("The MCP server is being run in a Node.js environment where `fetch` is not available. You won't be able to access any memories.\n\nAs the coding AI assistant, PLEASE ABORT THE CURRENT REQUEST. No alternate approaches will work. Help the user fix this issue so they can proceed by letting them know that they need to run the MCP server with Node.js version 20 or higher.");let o=`${this.baseUrl}${e}`;return this.requestQueue.enqueue(async()=>{try{r.log(`Calling ${o}`);let i=e.includes("/memories")?6e4:3e4;return await this.httpClient.request(o,{...t,headers:{"Content-Type":"application/json","x-api-key":this.byteroverPublicApiKey,...t.headers},timeout:i})}catch(a){throw a instanceof C?(r.error(`Request to ${o} timed out`),new Error(`Request timed out after ${a.timeout}ms. The Byterover API might be slow or unresponsive.`)):a instanceof f?(r.error(`HTTP error ${a.status} from ${o}:`,a.statusText),{status:a.status,err:a.statusText||"Unknown error"}):a instanceof b?(r.error(`Network error calling ${o}:`,a.originalError.message),new Error(`Network error: ${a.originalError.message}. Please check your internet connection.`)):a instanceof Error?new Error(`Failed to make request to Byterover API: ${a.message}`):new Error(`Failed to make request to Byterover API: ${a}`)}})}async searchMemories(e,t=5){return this.isConfigured?this.request("/memories/search",{method:"POST",body:JSON.stringify({query:e,limit:t,userId:this.userId})}):(r.warn("Cannot search memories: ByteroverService is not configured with valid credentials"),{results:[]})}async createMemory(e){if(!this.isConfigured){r.warn("Cannot create memory: ByteroverService is not configured with valid credentials");return}return this.request("/memories",{method:"POST",body:JSON.stringify({messages:e,userId:this.userId})})}async sendServerStartupConfirmation(e){if(!this.isConfigured){r.warn("Cannot send server startup confirmation: ByteroverService is not configured with valid credentials");return}return this.request("/projects/setup-success",{method:"POST",body:JSON.stringify({userId:this.userId})})}getQueueStats(){return this.requestQueue.getStats()}};var Q={name:"Byterover MCP Server",version:"0.2.2"};function P(c="",e="",t={}){let o=new K(Q),a=new R(c,e,{httpTimeout:t.httpTimeout,maxRetries:t.maxRetries,maxConcurrentRequests:t.maxConcurrentRequests,healthCheckInterval:t.healthCheckInterval});return(t.httpTimeout||t.maxRetries||t.maxConcurrentRequests||t.healthCheckInterval)&&r.log("Applying advanced configuration to ByteroverService",{httpTimeout:t.httpTimeout,maxRetries:t.maxRetries,maxConcurrentRequests:t.maxConcurrentRequests,healthCheckInterval:t.healthCheckInterval}),U(o,a),r.isHTTP=t.isHTTP??!1,o.updateCredentials=(i,s)=>a.updateCredentials(i,s),o.isServiceConfigured=()=>a.isServiceConfigured(),o}function U(c,e){c.tool("search-memories","Search a curated knowledge base of past coding solutions and insights to find relevant help for your current task. Use 'query' to specify search terms and 'limit' to control the number of results based on your task's complexity.",{query:y.string(),limit:y.number()},async({query:t,limit:o})=>{try{r.log(`Searching for memories with query: ${t} and limit: ${o}`);let a=await e.searchMemories(t,o),i=[{type:"text",text:"Memories:"}];return a.results.forEach(s=>{i.push({type:"text",text:`Memory (score: ${s.score}): ${s.memory}${s.metadata?.tags?`
Tags: ${s.metadata.tags.join(", ")}`:""}`})}),a.relations&&a.relations.length>0&&(i.push({type:"text",text:"Relations:"}),a.relations.forEach(s=>{i.push({type:"text",text:`${s.source} ${s.relationship} ${s.destination}`})})),{content:i}}catch(a){return r.error(`Error searching for memories: ${a}`),{content:[{type:"text",text:"Failed to search for memories"}]}}}),c.tool("create-memories","Create a new memory by capturing key coding insights from human-agent interactions to help solve similar tasks in the future.",{messages:y.array(y.object({role:y.enum(["user","assistant"]),content:y.string()}))},async({messages:t})=>{try{return r.log(`Creating memory with ${t.length} messages`),await e.createMemory(t),{content:[{type:"text",text:"Memory created successfully"}]}}catch(o){return r.error(`Error creating memory: ${o}`),{content:[{type:"text",text:"Failed to create memory"}]}}})}async function F(){let c=process.env.NODE_ENV==="cli"||process.argv.includes("--stdio"),e=I(c),t=P(e.byteroverPublicApiKey,e.userId,{isHTTP:!c,httpTimeout:e.httpTimeout,maxRetries:e.maxRetries,maxConcurrentRequests:e.maxConcurrentRequests,healthCheckInterval:e.healthCheckInterval});if(c){let o=new j;await t.connect(o)}else e.isConfigured?console.log(`Initializing Byterover MCP Server in HTTP mode on port ${e.port}...`):(console.log(`Initializing Byterover MCP Server in HTTP mode on port ${e.port} with limited functionality...`),console.log("Use the /config endpoint to set up your Byterover credentials."),console.log(`Example: curl -X POST http://localhost:${e.port}/config -H "Content-Type: application/json" -d '{"byteroverPublicApiKey":"YOUR_API_KEY","userId":"YOUR_USER_ID"}'`)),await w(e.port,t)}process.argv[1]&&F().catch(c=>{console.error("Failed to start server:",c),process.exit(1)});export{I as a,P as b,F as c};