@kynver-app/mcp-inbox 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/sse.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{StdioServerTransport as
|
|
2
|
+
import{StdioServerTransport as Y}from"@modelcontextprotocol/sdk/server/stdio.js";import{existsSync as P,readFileSync as A}from"node:fs";import{dirname as c,join as g}from"node:path";import{fileURLToPath as u}from"node:url";function N(e){let n=c(u(e));for(let i=0;i<6;i+=1){if(P(g(n,"package.json")))return n;let r=c(n);if(r===n)break;n=r}throw new Error(`package.json not found above ${c(u(e))}`)}function T(e,n){let i=g(N(e),"package.json"),o=JSON.parse(A(i,"utf8"))[n];if(typeof o!="string"||!o.trim())throw new Error(`Missing package.json ${n} at ${i}`);return o}function d(e=import.meta.url){return T(e,"version")}function E(e){return e.some(n=>n==="--version"||n==="-v")}function K(e,n){console.log(n?`${n} ${e}`:e),process.exit(0)}function f(e,n=import.meta.url,i){return E(e)?(K(d(n),i),!0):!1}import{McpServer as j}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as t}from"zod";import{AsyncLocalStorage as I}from"node:async_hooks";var _=new I,l=process.env.KYNVER_API_KEY??"";function k(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let n;try{n=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let i=n.hostname==="localhost"||n.hostname==="127.0.0.1";if(n.protocol!=="https:"&&!(n.protocol==="http:"&&i))throw new Error(`KYNVER_API_URL must be https (got "${n.protocol}//${n.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function O(){return k()}function C(e){let n=e.startsWith("/")?e:`/${e}`;if(n.includes("..")||n.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function x(e){C(e);let n=e.startsWith("/")?e:`/${e}`,i=n.startsWith("/api")?n:`/api${n}`;return`${O()}${i}`}function y(){let e={"Content-Type":"application/json"};l&&(e.Authorization=`Bearer ${l}`);let n=_.getStore(),i=n?.userId??process.env.KYNVER_USER_ID?.trim(),r=process.env.KYNVER_SERVICE_SECRET?.trim();if(i){if(n?.sessionCredential)return n.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=n.sessionCredential):e.Authorization=`Bearer ${n.sessionCredential}`,e;let o=process.env.KYNVER_RUNNER_TOKEN?.trim();if(o){if(n?.authKind==="apiKey"||n?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(n?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=o,e}if(!r)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=i,e["X-Kynver-Service-Secret"]=r}return e}async function b(e){let n=await e.text();if(!e.ok){let i=n;try{let r=JSON.parse(n);i=r.error??r.message??n}catch{}throw new Error(i||`HTTP ${e.status}`)}if(n)try{return JSON.parse(n)}catch{return n}}async function s(e){let n=await fetch(x(e),{method:"GET",headers:y()});return b(n)}async function a(e,n){let i=await fetch(x(e),{method:"POST",headers:y(),body:JSON.stringify(n)});return b(i)}function h(){return!!l}function w(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}function p(e,n,i,r){return{name:e,description:n,inputSchemaZod:i,execute:async o=>h()?r(o):w()}}var m={inboxAddress:t.string().email().max(254).optional().describe("Admin only: use this owned inbox instead of your primary one (e.g. a second address)"),inboxId:t.string().max(64).optional().describe("Admin only: owned inbox id instead of your primary one")};function R(e,n){return n.inboxAddress&&e.set("inboxAddress",n.inboxAddress),n.inboxId&&e.set("inboxId",n.inboxId),e}var L=p("inbox_list","List recent messages in your Kynver inbox, or read one message in full when id is set. Returns compact from/to/subject/preview unless reading a single id. Admins may pass inboxAddress/inboxId to read an additional owned inbox.",t.object({id:t.string().optional().describe("Message id to read in full"),limit:t.number().int().min(1).max(50).optional().describe("Max messages to list (default 20)"),...m}),async e=>{let n=R(new URLSearchParams,e);e.id&&n.set("id",e.id),e.limit&&n.set("limit",String(e.limit));let i=n.toString();return JSON.stringify(await s(i?`/agent-inbox/messages?${i}`:"/agent-inbox/messages"))}),V=p("inbox_send","Send email from your Kynver inbox address. Replies thread when inReplyTo is the prior message id header. Admins may pass inboxAddress/inboxId to send from an additional owned inbox.",t.object({to:t.union([t.string().email(),t.array(t.string().email()).min(1).max(50)]).describe("Recipient email or list; 50 total across to/cc/bcc"),cc:t.union([t.string().email(),t.array(t.string().email()).min(1).max(50)]).optional(),bcc:t.union([t.string().email(),t.array(t.string().email()).min(1).max(50)]).optional(),subject:t.string().min(1).max(998).describe("Subject line"),text:t.string().min(1).max(1e5).describe("Plain-text body"),inReplyTo:t.string().max(998).optional().describe("Prior RFC Message-ID, not Resend UUID"),references:t.array(t.string().max(998)).min(1).max(20).optional().describe("Ordered RFC Message-IDs; inReplyTo is appended if needed"),attachments:t.array(t.object({filename:t.string().min(1).max(255),content:t.string().min(4).max(4194304).describe("Canonical base64 bytes, not a URL/path. 2 MiB total decoded"),contentType:t.string().max(127).optional()}).strict()).max(10).optional(),idempotencyKey:t.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/).optional().describe("Unique logical-send key; reuse identical payload/key on retries within provider retention, never for a new send"),...m}),async e=>JSON.stringify(await a("/agent-inbox/send",e))),$=p("inbox_address","Get your Kynver inbox address, or create one on first use. Optional localPart is the local-part on the existing Kynver mail domain. Admins: additional=true with localPart creates (idempotently) an extra inbox; list=true lists every owned inbox; inboxAddress/inboxId reads one.",t.object({localPart:t.string().optional().describe("Requested local part on first create (ignored if you already have an address), or of the additional inbox"),displayName:t.string().optional().describe("From-name on first create or for the additional inbox"),additional:t.boolean().optional().describe("Admin only: create an additional inbox at localPart"),list:t.boolean().optional().describe("Admin only: list every inbox you own, primary first"),...m}),async e=>{if(e.additional)return e.localPart?JSON.stringify(await a("/agent-inbox/inboxes",{localPart:e.localPart,displayName:e.displayName})):JSON.stringify({error:"localPart is required when additional=true"});if(e.list)return JSON.stringify(await s("/agent-inbox/inboxes"));if(e.inboxAddress||e.inboxId)return JSON.stringify(await s(`/agent-inbox?${R(new URLSearchParams,e)}`));if(e.localPart||e.displayName)return JSON.stringify(await a("/agent-inbox",{localPart:e.localPart,displayName:e.displayName}));let n=await s("/agent-inbox");return n.inbox?JSON.stringify(n):JSON.stringify(await a("/agent-inbox",{}))}),S=[L,V,$];var J=d(import.meta.url);function M(e){return{content:[{type:"text",text:e}]}}function v(){let e=new j({name:"kynver-mcp-inbox",version:J},{capabilities:{tools:{}}});for(let n of S)e.registerTool(n.name,{description:n.description,inputSchema:n.inputSchemaZod},async i=>M(await n.execute(i)));return e}f(process.argv.slice(2),import.meta.url,"kynver-mcp-inbox");var q=v(),z=new Y;await q.connect(z);
|
package/dist/sse.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as oe}from"node:http";import{randomUUID as se,timingSafeEqual as ie}from"node:crypto";import{SSEServerTransport as ae}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as ce}from"@modelcontextprotocol/sdk/server/streamableHttp.js";var ue=1*1024*1024,le=["apiKey","secret","userId"],de=new Set(["1","true","yes","on"]);function pe(){let e=process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();return e!==void 0&&de.has(e)?!1:process.env.NODE_ENV==="production"||process.env.VERCEL_ENV==="production"}function v(e,t){let r=e.headers[t];return typeof r=="string"?r:Array.isArray(r)?r[0]??null:null}function D(e,t){let r=Buffer.from(e),o=Buffer.from(t);return r.length!==o.length?!1:ie(r,o)}function j(e){return le.some(t=>e.searchParams.has(t))}function me(){return process.env.MCP_SSE_HOST?.trim()||"127.0.0.1"}function x(e,t){let r=process.env[e];if(!r)return t;let o=parseInt(r,10);return Number.isFinite(o)&&o>0?o:t}function fe(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let r=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&r))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function Y(e){let t=v(e,"x-forwarded-for");return t?t.split(",")[0].trim():e.socket?.remoteAddress??"unknown"}function ge(e,t){let r=new Map;return{check(o){let c=Date.now(),l=r.get(o);return!l||c>=l.resetAt?(r.set(o,{count:1,resetAt:c+t}),!0):l.count>=e?!1:(l.count+=1,!0)}}}function T(e,t=ue){return new Promise((r,o)=>{let c=[],l=0;e.on("data",f=>{if(l+=f.length,l>t){e.destroy(),o(new Error("Request body too large"));return}c.push(f)}),e.on("end",()=>{try{r(JSON.parse(Buffer.concat(c).toString()))}catch(f){o(f)}}),e.on("error",o)})}function P(e){let t=v(e,"authorization");return t?/^Bearer\s+(.+)$/i.exec(t)?.[1]?.trim()??"":""}function _(e,t){let r=v(e,"x-kynver-user-id"),o=v(e,"x-kynver-service-secret");return!r||!t||!o||pe()||!D(o,t)?null:{userId:r,auth:{kind:"internal",userId:r}}}function z(e,t,r,o){if(r.kind==="apiKey"){let l=P(e);return l.length>0&&D(l,r.apiKey)}let c=_(e,o);return c!==null&&c.userId===r.userId&&c.auth.kind==="internal"}async function B(e,t){try{let r=await fetch(`${e.replace(/\/$/,"")}/api/users/me`,{headers:{Authorization:`Bearer ${t}`}});return r.ok?(await r.json()).id??null:null}catch{return null}}function he(e,t){return new URL(e.url??"/",`http://localhost:${t}`)}function a(e,t,r){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(r))}function X(e){let t=parseInt(process.env.PORT??String(e.defaultPort),10),r=fe(),o=process.env.KYNVER_SERVICE_SECRET??"",c=me(),l={},f={},O=x("MCP_SSE_MAX_SESSIONS",200),C=x("MCP_SSE_MAX_SESSIONS_PER_USER",20),U=x("MCP_SSE_IDLE_TIMEOUT_MS",300*1e3),re=x("MCP_SSE_RATE_LIMIT_PER_MIN",120),w=ge(re,60*1e3);setInterval(()=>{let s=Date.now();for(let[n,g]of Object.entries(l))s-g.lastActivity>U&&(delete l[n],g.transport.close().catch(()=>{}));for(let[n,g]of Object.entries(f))s-g.lastActivity>U&&(delete f[n],g.transport.close().catch(()=>{}))},60*1e3).unref?.();let L=()=>Object.keys(l).length+Object.keys(f).length,$=s=>Object.values(l).filter(n=>n.userId===s).length+Object.values(f).filter(n=>n.userId===s).length,V=oe(async(s,n)=>{let g=he(s,t),E=g.pathname;if(s.method==="GET"&&E==="/sse"){if(!w.check(`sse:${Y(s)}`)){a(n,429,{error:"Too many requests. Slow down and retry."});return}if(j(g)){a(n,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let p=null,u=null,m=o?_(s,o):null;if(m)p=m.userId,u=m.auth;else{let y=P(s);y&&(p=await B(r,y),p&&(u={kind:"apiKey",apiKey:y}))}if(!p||!u){a(n,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(L()>=O){a(n,503,{error:"Server at capacity. Try again shortly."});return}if($(p)>=C){a(n,429,{error:"Too many concurrent connections for this account."});return}console.log(`[MCP SSE] New connection (${e.label}, user: ${p.slice(0,8)}...)`);let h=new ae("/messages",n);l[h.sessionId]={transport:h,userId:p,auth:u,lastActivity:Date.now()},n.on("close",()=>{delete l[h.sessionId]}),await(await e.createMcpServer()).connect(h);return}if(s.method==="POST"&&E==="/messages"){let p=g.searchParams.get("sessionId"),u=p?l[p]:void 0;if(!u){a(n,400,{error:"No active SSE session for this sessionId"});return}if(!z(s,g,u.auth,o)){a(n,401,{error:"Unauthorized. Re-send the same credentials used to open the SSE session."});return}if(!w.check(`msg:${u.userId}`)){a(n,429,{error:"Too many requests. Slow down and retry."});return}u.lastActivity=Date.now();try{let m=await T(s);await e.requestContext.run({userId:u.userId,authKind:u.auth.kind,sessionCredential:u.auth.kind==="apiKey"?u.auth.apiKey:void 0},async()=>{await u.transport.handlePostMessage(s,n,m)})}catch(m){a(n,400,{error:m instanceof Error?m.message:"Invalid request body"})}return}if(E==="/mcp"){if(s.method!=="POST"&&s.method!=="GET"&&s.method!=="DELETE"){n.writeHead(405,{"Content-Type":"application/json",Allow:"GET, POST, DELETE"}),n.end(JSON.stringify({error:"Method not allowed"}));return}if(j(g)){a(n,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let p=v(s,"mcp-session-id");if(p){let d=f[p];if(!d){a(n,404,{error:"Unknown or expired MCP session. Re-initialize."});return}if(!z(s,g,d.auth,o)){a(n,401,{error:"Unauthorized. Re-send the same credentials used to initialize the MCP session."});return}if(!w.check(`msg:${d.userId}`)){a(n,429,{error:"Too many requests. Slow down and retry."});return}d.lastActivity=Date.now();try{let I=s.method==="POST"?await T(s):void 0;await e.requestContext.run({userId:d.userId},async()=>{await d.transport.handleRequest(s,n,I)})}catch(I){n.headersSent||a(n,400,{error:I instanceof Error?I.message:"Invalid request body"})}return}if(s.method!=="POST"){a(n,400,{error:"Missing mcp-session-id header. Open a session with an initialize POST first."});return}if(!w.check(`mcp:${Y(s)}`)){a(n,429,{error:"Too many requests. Slow down and retry."});return}let u=null,m=null,h=o?_(s,o):null;if(h)u=h.userId,m=h.auth;else{let d=P(s);d&&(u=await B(r,d),u&&(m={kind:"apiKey",apiKey:d}))}if(!u||!m){a(n,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(L()>=O){a(n,503,{error:"Server at capacity. Try again shortly."});return}if($(u)>=C){a(n,429,{error:"Too many concurrent connections for this account."});return}let b;try{b=await T(s)}catch(d){a(n,400,{error:d instanceof Error?d.message:"Invalid request body"});return}let y=u,ne=m,S=new ce({sessionIdGenerator:()=>se(),onsessioninitialized:d=>{f[d]={transport:S,userId:y,auth:ne,lastActivity:Date.now()},console.log(`[MCP HTTP] New session (${e.label}, user: ${y.slice(0,8)}...)`)}});S.onclose=()=>{S.sessionId&&delete f[S.sessionId]},await(await e.createMcpServer()).connect(S);try{await e.requestContext.run({userId:y},async()=>{await S.handleRequest(s,n,b)})}catch(d){n.headersSent||a(n,400,{error:d instanceof Error?d.message:"Invalid request"})}return}if(s.method==="GET"&&E==="/health"){n.writeHead(200,{"Content-Type":"text/plain"}),n.end("ok");return}a(n,404,{error:"Not found"})});return V.listen(t,c,()=>{console.log(`[MCP SSE] ${e.label} listening on ${c}:${t}`)}),V}import{McpServer as Ke}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as i}from"zod";import{AsyncLocalStorage as ye}from"node:async_hooks";var A=new ye,K=process.env.KYNVER_API_KEY??"";function Se(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let r=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&r))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function ve(){return Se()}function we(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function J(e){we(e);let t=e.startsWith("/")?e:`/${e}`,r=t.startsWith("/api")?t:`/api${t}`;return`${ve()}${r}`}function F(){let e={"Content-Type":"application/json"};K&&(e.Authorization=`Bearer ${K}`);let t=A.getStore(),r=t?.userId??process.env.KYNVER_USER_ID?.trim(),o=process.env.KYNVER_SERVICE_SECRET?.trim();if(r){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let c=process.env.KYNVER_RUNNER_TOKEN?.trim();if(c){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=c,e}if(!o)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=r,e["X-Kynver-Service-Secret"]=o}return e}async function H(e){let t=await e.text();if(!e.ok){let r=t;try{let o=JSON.parse(t);r=o.error??o.message??t}catch{}throw new Error(r||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function k(e){let t=await fetch(J(e),{method:"GET",headers:F()});return H(t)}async function R(e,t){let r=await fetch(J(e),{method:"POST",headers:F(),body:JSON.stringify(t)});return H(r)}function G(){return!!K}function W(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}function M(e,t,r,o){return{name:e,description:t,inputSchemaZod:r,execute:async c=>G()?o(c):W()}}var Ie=M("inbox_list","List recent messages in your Kynver inbox, or read one message in full when id is set. Returns compact from/to/subject/preview unless reading a single id.",i.object({id:i.string().optional().describe("Message id to read in full"),limit:i.number().int().min(1).max(50).optional().describe("Max messages to list (default 20)")}),async e=>{let t=new URLSearchParams;e.id&&t.set("id",e.id),e.limit&&t.set("limit",String(e.limit));let r=t.toString();return JSON.stringify(await k(r?`/agent-inbox/messages?${r}`:"/agent-inbox/messages"))}),xe=M("inbox_send","Send email from your Kynver inbox address. Replies thread when inReplyTo is the prior message id header.",i.object({to:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).describe("Recipient email or list; 50 total across to/cc/bcc"),cc:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).optional(),bcc:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).optional(),subject:i.string().min(1).max(998).describe("Subject line"),text:i.string().min(1).max(1e5).describe("Plain-text body"),inReplyTo:i.string().max(998).optional().describe("Prior RFC Message-ID, not Resend UUID"),references:i.array(i.string().max(998)).min(1).max(20).optional().describe("Ordered RFC Message-IDs; inReplyTo is appended if needed"),attachments:i.array(i.object({filename:i.string().min(1).max(255),content:i.string().min(4).max(4194304).describe("Canonical base64 bytes, not a URL/path. 2 MiB total decoded"),contentType:i.string().max(127).optional()}).strict()).max(10).optional(),idempotencyKey:i.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/).optional().describe("Unique logical-send key; reuse identical payload/key on retries within provider retention, never for a new send")}),async e=>JSON.stringify(await R("/agent-inbox/send",e))),Re=M("inbox_address","Get your Kynver inbox address, or create one on first use. Optional localPart is the local-part on the existing Kynver mail domain.",i.object({localPart:i.string().optional().describe("Requested local part on first create (ignored if you already have an address)"),displayName:i.string().optional().describe("From-name on first create")}),async e=>{if(e.localPart||e.displayName)return JSON.stringify(await R("/agent-inbox",e));let t=await k("/agent-inbox");return t.inbox?JSON.stringify(t):JSON.stringify(await R("/agent-inbox",{}))}),Z=[Ie,xe,Re];import{existsSync as be,readFileSync as Te}from"node:fs";import{dirname as N,join as q}from"node:path";import{fileURLToPath as Q}from"node:url";function Pe(e){let t=N(Q(e));for(let r=0;r<6;r+=1){if(be(q(t,"package.json")))return t;let o=N(t);if(o===t)break;t=o}throw new Error(`package.json not found above ${N(Q(e))}`)}function _e(e,t){let r=q(Pe(e),"package.json"),c=JSON.parse(Te(r,"utf8"))[t];if(typeof c!="string"||!c.trim())throw new Error(`Missing package.json ${t} at ${r}`);return c}function ee(e=import.meta.url){return _e(e,"version")}var Ae=ee(import.meta.url);function ke(e){return{content:[{type:"text",text:e}]}}function te(){let e=new Ke({name:"kynver-mcp-inbox",version:Ae},{capabilities:{tools:{}}});for(let t of Z)e.registerTool(t.name,{description:t.description,inputSchema:t.inputSchemaZod},async r=>ke(await t.execute(r)));return e}X({label:"Inbox server",defaultPort:3109,createMcpServer:te,requestContext:A});
|
|
1
|
+
import{createServer as se}from"node:http";import{randomUUID as ae,timingSafeEqual as ce}from"node:crypto";import{SSEServerTransport as ue}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as le}from"@modelcontextprotocol/sdk/server/streamableHttp.js";var de=1*1024*1024,pe=["apiKey","secret","userId"],me=new Set(["1","true","yes","on"]);function fe(){let e=process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();return e!==void 0&&me.has(e)?!1:process.env.NODE_ENV==="production"||process.env.VERCEL_ENV==="production"}function v(e,t){let n=e.headers[t];return typeof n=="string"?n:Array.isArray(n)?n[0]??null:null}function J(e,t){let n=Buffer.from(e),o=Buffer.from(t);return n.length!==o.length?!1:ce(n,o)}function Y(e){return pe.some(t=>e.searchParams.has(t))}function ye(){return process.env.MCP_SSE_HOST?.trim()||"127.0.0.1"}function R(e,t){let n=process.env[e];if(!n)return t;let o=parseInt(n,10);return Number.isFinite(o)&&o>0?o:t}function ge(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let n=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&n))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function z(e){let t=v(e,"x-forwarded-for");return t?t.split(",")[0].trim():e.socket?.remoteAddress??"unknown"}function he(e,t){let n=new Map;return{check(o){let c=Date.now(),l=n.get(o);return!l||c>=l.resetAt?(n.set(o,{count:1,resetAt:c+t}),!0):l.count>=e?!1:(l.count+=1,!0)}}}function P(e,t=de){return new Promise((n,o)=>{let c=[],l=0;e.on("data",f=>{if(l+=f.length,l>t){e.destroy(),o(new Error("Request body too large"));return}c.push(f)}),e.on("end",()=>{try{n(JSON.parse(Buffer.concat(c).toString()))}catch(f){o(f)}}),e.on("error",o)})}function A(e){let t=v(e,"authorization");return t?/^Bearer\s+(.+)$/i.exec(t)?.[1]?.trim()??"":""}function _(e,t){let n=v(e,"x-kynver-user-id"),o=v(e,"x-kynver-service-secret");return!n||!t||!o||fe()||!J(o,t)?null:{userId:n,auth:{kind:"internal",userId:n}}}function B(e,t,n,o){if(n.kind==="apiKey"){let l=A(e);return l.length>0&&J(l,n.apiKey)}let c=_(e,o);return c!==null&&c.userId===n.userId&&c.auth.kind==="internal"}async function D(e,t){try{let n=await fetch(`${e.replace(/\/$/,"")}/api/users/me`,{headers:{Authorization:`Bearer ${t}`}});return n.ok?(await n.json()).id??null:null}catch{return null}}function Se(e,t){return new URL(e.url??"/",`http://localhost:${t}`)}function a(e,t,n){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(n))}function X(e){let t=parseInt(process.env.PORT??String(e.defaultPort),10),n=ge(),o=process.env.KYNVER_SERVICE_SECRET??"",c=ye(),l={},f={},U=R("MCP_SSE_MAX_SESSIONS",200),C=R("MCP_SSE_MAX_SESSIONS_PER_USER",20),L=R("MCP_SSE_IDLE_TIMEOUT_MS",300*1e3),oe=R("MCP_SSE_RATE_LIMIT_PER_MIN",120),w=he(oe,60*1e3);setInterval(()=>{let s=Date.now();for(let[r,y]of Object.entries(l))s-y.lastActivity>L&&(delete l[r],y.transport.close().catch(()=>{}));for(let[r,y]of Object.entries(f))s-y.lastActivity>L&&(delete f[r],y.transport.close().catch(()=>{}))},60*1e3).unref?.();let $=()=>Object.keys(l).length+Object.keys(f).length,V=s=>Object.values(l).filter(r=>r.userId===s).length+Object.values(f).filter(r=>r.userId===s).length,j=se(async(s,r)=>{let y=Se(s,t),I=y.pathname;if(s.method==="GET"&&I==="/sse"){if(!w.check(`sse:${z(s)}`)){a(r,429,{error:"Too many requests. Slow down and retry."});return}if(Y(y)){a(r,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let p=null,u=null,m=o?_(s,o):null;if(m)p=m.userId,u=m.auth;else{let h=A(s);h&&(p=await D(n,h),p&&(u={kind:"apiKey",apiKey:h}))}if(!p||!u){a(r,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if($()>=U){a(r,503,{error:"Server at capacity. Try again shortly."});return}if(V(p)>=C){a(r,429,{error:"Too many concurrent connections for this account."});return}console.log(`[MCP SSE] New connection (${e.label}, user: ${p.slice(0,8)}...)`);let g=new ue("/messages",r);l[g.sessionId]={transport:g,userId:p,auth:u,lastActivity:Date.now()},r.on("close",()=>{delete l[g.sessionId]}),await(await e.createMcpServer()).connect(g);return}if(s.method==="POST"&&I==="/messages"){let p=y.searchParams.get("sessionId"),u=p?l[p]:void 0;if(!u){a(r,400,{error:"No active SSE session for this sessionId"});return}if(!B(s,y,u.auth,o)){a(r,401,{error:"Unauthorized. Re-send the same credentials used to open the SSE session."});return}if(!w.check(`msg:${u.userId}`)){a(r,429,{error:"Too many requests. Slow down and retry."});return}u.lastActivity=Date.now();try{let m=await P(s);await e.requestContext.run({userId:u.userId,authKind:u.auth.kind,sessionCredential:u.auth.kind==="apiKey"?u.auth.apiKey:void 0},async()=>{await u.transport.handlePostMessage(s,r,m)})}catch(m){a(r,400,{error:m instanceof Error?m.message:"Invalid request body"})}return}if(I==="/mcp"){if(s.method!=="POST"&&s.method!=="GET"&&s.method!=="DELETE"){r.writeHead(405,{"Content-Type":"application/json",Allow:"GET, POST, DELETE"}),r.end(JSON.stringify({error:"Method not allowed"}));return}if(Y(y)){a(r,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let p=v(s,"mcp-session-id");if(p){let d=f[p];if(!d){a(r,404,{error:"Unknown or expired MCP session. Re-initialize."});return}if(!B(s,y,d.auth,o)){a(r,401,{error:"Unauthorized. Re-send the same credentials used to initialize the MCP session."});return}if(!w.check(`msg:${d.userId}`)){a(r,429,{error:"Too many requests. Slow down and retry."});return}d.lastActivity=Date.now();try{let E=s.method==="POST"?await P(s):void 0;await e.requestContext.run({userId:d.userId},async()=>{await d.transport.handleRequest(s,r,E)})}catch(E){r.headersSent||a(r,400,{error:E instanceof Error?E.message:"Invalid request body"})}return}if(s.method!=="POST"){a(r,400,{error:"Missing mcp-session-id header. Open a session with an initialize POST first."});return}if(!w.check(`mcp:${z(s)}`)){a(r,429,{error:"Too many requests. Slow down and retry."});return}let u=null,m=null,g=o?_(s,o):null;if(g)u=g.userId,m=g.auth;else{let d=A(s);d&&(u=await D(n,d),u&&(m={kind:"apiKey",apiKey:d}))}if(!u||!m){a(r,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if($()>=U){a(r,503,{error:"Server at capacity. Try again shortly."});return}if(V(u)>=C){a(r,429,{error:"Too many concurrent connections for this account."});return}let T;try{T=await P(s)}catch(d){a(r,400,{error:d instanceof Error?d.message:"Invalid request body"});return}let h=u,ie=m,S=new le({sessionIdGenerator:()=>ae(),onsessioninitialized:d=>{f[d]={transport:S,userId:h,auth:ie,lastActivity:Date.now()},console.log(`[MCP HTTP] New session (${e.label}, user: ${h.slice(0,8)}...)`)}});S.onclose=()=>{S.sessionId&&delete f[S.sessionId]},await(await e.createMcpServer()).connect(S);try{await e.requestContext.run({userId:h},async()=>{await S.handleRequest(s,r,T)})}catch(d){r.headersSent||a(r,400,{error:d instanceof Error?d.message:"Invalid request"})}return}if(s.method==="GET"&&I==="/health"){r.writeHead(200,{"Content-Type":"text/plain"}),r.end("ok");return}a(r,404,{error:"Not found"})});return j.listen(t,c,()=>{console.log(`[MCP SSE] ${e.label} listening on ${c}:${t}`)}),j}import{McpServer as ke}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as i}from"zod";import{AsyncLocalStorage as ve}from"node:async_hooks";var k=new ve,K=process.env.KYNVER_API_KEY??"";function xe(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let n=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&n))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function be(){return xe()}function we(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function F(e){we(e);let t=e.startsWith("/")?e:`/${e}`,n=t.startsWith("/api")?t:`/api${t}`;return`${be()}${n}`}function H(){let e={"Content-Type":"application/json"};K&&(e.Authorization=`Bearer ${K}`);let t=k.getStore(),n=t?.userId??process.env.KYNVER_USER_ID?.trim(),o=process.env.KYNVER_SERVICE_SECRET?.trim();if(n){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let c=process.env.KYNVER_RUNNER_TOKEN?.trim();if(c){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=c,e}if(!o)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=n,e["X-Kynver-Service-Secret"]=o}return e}async function G(e){let t=await e.text();if(!e.ok){let n=t;try{let o=JSON.parse(t);n=o.error??o.message??t}catch{}throw new Error(n||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function x(e){let t=await fetch(F(e),{method:"GET",headers:H()});return G(t)}async function b(e,t){let n=await fetch(F(e),{method:"POST",headers:H(),body:JSON.stringify(t)});return G(n)}function W(){return!!K}function Z(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}function N(e,t,n,o){return{name:e,description:t,inputSchemaZod:n,execute:async c=>W()?o(c):Z()}}var M={inboxAddress:i.string().email().max(254).optional().describe("Admin only: use this owned inbox instead of your primary one (e.g. a second address)"),inboxId:i.string().max(64).optional().describe("Admin only: owned inbox id instead of your primary one")};function Q(e,t){return t.inboxAddress&&e.set("inboxAddress",t.inboxAddress),t.inboxId&&e.set("inboxId",t.inboxId),e}var Ee=N("inbox_list","List recent messages in your Kynver inbox, or read one message in full when id is set. Returns compact from/to/subject/preview unless reading a single id. Admins may pass inboxAddress/inboxId to read an additional owned inbox.",i.object({id:i.string().optional().describe("Message id to read in full"),limit:i.number().int().min(1).max(50).optional().describe("Max messages to list (default 20)"),...M}),async e=>{let t=Q(new URLSearchParams,e);e.id&&t.set("id",e.id),e.limit&&t.set("limit",String(e.limit));let n=t.toString();return JSON.stringify(await x(n?`/agent-inbox/messages?${n}`:"/agent-inbox/messages"))}),Re=N("inbox_send","Send email from your Kynver inbox address. Replies thread when inReplyTo is the prior message id header. Admins may pass inboxAddress/inboxId to send from an additional owned inbox.",i.object({to:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).describe("Recipient email or list; 50 total across to/cc/bcc"),cc:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).optional(),bcc:i.union([i.string().email(),i.array(i.string().email()).min(1).max(50)]).optional(),subject:i.string().min(1).max(998).describe("Subject line"),text:i.string().min(1).max(1e5).describe("Plain-text body"),inReplyTo:i.string().max(998).optional().describe("Prior RFC Message-ID, not Resend UUID"),references:i.array(i.string().max(998)).min(1).max(20).optional().describe("Ordered RFC Message-IDs; inReplyTo is appended if needed"),attachments:i.array(i.object({filename:i.string().min(1).max(255),content:i.string().min(4).max(4194304).describe("Canonical base64 bytes, not a URL/path. 2 MiB total decoded"),contentType:i.string().max(127).optional()}).strict()).max(10).optional(),idempotencyKey:i.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/).optional().describe("Unique logical-send key; reuse identical payload/key on retries within provider retention, never for a new send"),...M}),async e=>JSON.stringify(await b("/agent-inbox/send",e))),Te=N("inbox_address","Get your Kynver inbox address, or create one on first use. Optional localPart is the local-part on the existing Kynver mail domain. Admins: additional=true with localPart creates (idempotently) an extra inbox; list=true lists every owned inbox; inboxAddress/inboxId reads one.",i.object({localPart:i.string().optional().describe("Requested local part on first create (ignored if you already have an address), or of the additional inbox"),displayName:i.string().optional().describe("From-name on first create or for the additional inbox"),additional:i.boolean().optional().describe("Admin only: create an additional inbox at localPart"),list:i.boolean().optional().describe("Admin only: list every inbox you own, primary first"),...M}),async e=>{if(e.additional)return e.localPart?JSON.stringify(await b("/agent-inbox/inboxes",{localPart:e.localPart,displayName:e.displayName})):JSON.stringify({error:"localPart is required when additional=true"});if(e.list)return JSON.stringify(await x("/agent-inbox/inboxes"));if(e.inboxAddress||e.inboxId)return JSON.stringify(await x(`/agent-inbox?${Q(new URLSearchParams,e)}`));if(e.localPart||e.displayName)return JSON.stringify(await b("/agent-inbox",{localPart:e.localPart,displayName:e.displayName}));let t=await x("/agent-inbox");return t.inbox?JSON.stringify(t):JSON.stringify(await b("/agent-inbox",{}))}),q=[Ee,Re,Te];import{existsSync as Pe,readFileSync as Ae}from"node:fs";import{dirname as O,join as te}from"node:path";import{fileURLToPath as ee}from"node:url";function _e(e){let t=O(ee(e));for(let n=0;n<6;n+=1){if(Pe(te(t,"package.json")))return t;let o=O(t);if(o===t)break;t=o}throw new Error(`package.json not found above ${O(ee(e))}`)}function Ke(e,t){let n=te(_e(e),"package.json"),c=JSON.parse(Ae(n,"utf8"))[t];if(typeof c!="string"||!c.trim())throw new Error(`Missing package.json ${t} at ${n}`);return c}function ne(e=import.meta.url){return Ke(e,"version")}var Ne=ne(import.meta.url);function Me(e){return{content:[{type:"text",text:e}]}}function re(){let e=new ke({name:"kynver-mcp-inbox",version:Ne},{capabilities:{tools:{}}});for(let t of q)e.registerTool(t.name,{description:t.description,inputSchema:t.inputSchemaZod},async n=>Me(await t.execute(n)));return e}X({label:"Inbox server",defaultPort:3109,createMcpServer:re,requestContext:k});
|