@kynver-app/mcp-meds 0.0.0-stage → 0.1.0

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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{StdioServerTransport as se}from"@modelcontextprotocol/sdk/server/stdio.js";import{existsSync as $,readFileSync as j}from"node:fs";import{dirname as l,join as b}from"node:path";import{fileURLToPath as y}from"node:url";function U(e){let t=l(y(e));for(let o=0;o<6;o+=1){if($(b(t,"package.json")))return t;let r=l(t);if(r===t)break;t=r}throw new Error(`package.json not found above ${l(y(e))}`)}function D(e,t){let o=b(U(e),"package.json"),s=JSON.parse(j(o,"utf8"))[t];if(typeof s!="string"||!s.trim())throw new Error(`Missing package.json ${t} at ${o}`);return s}function u(e=import.meta.url){return D(e,"version")}function F(e){return e.some(t=>t==="--version"||t==="-v")}function Y(e,t){console.log(t?`${t} ${e}`:e),process.exit(0)}function T(e,t=import.meta.url,o){return F(e)?(Y(u(t),o),!0):!1}import{readFile as X}from"node:fs/promises";import{extname as Z}from"node:path";import{z as c}from"zod";import{AsyncLocalStorage as z}from"node:async_hooks";var B=new z,h=process.env.KYNVER_API_KEY??"";function H(){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 o=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&o))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 J(){return H()}function q(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 m(e){q(e);let t=e.startsWith("/")?e:`/${e}`,o=t.startsWith("/api")?t:`/api${t}`;return`${J()}${o}`}function d(){let e={"Content-Type":"application/json"};h&&(e.Authorization=`Bearer ${h}`);let t=B.getStore(),o=t?.userId??process.env.KYNVER_USER_ID?.trim(),r=process.env.KYNVER_SERVICE_SECRET?.trim();if(o){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let s=process.env.KYNVER_RUNNER_TOKEN?.trim();if(s){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"]=s,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"]=o,e["X-Kynver-Service-Secret"]=r}return e}async function p(e){let t=await e.text();if(!e.ok){let o=t;try{let r=JSON.parse(t);o=r.error??r.message??t}catch{}throw new Error(o||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function x(e){let t=await fetch(m(e),{method:"GET",headers:d()});return p(t)}async function E(e,t){let o=await fetch(m(e),{method:"POST",headers:d(),body:JSON.stringify(t)});return p(o)}async function f(e,t){let o=await fetch(m(e),{method:"PATCH",headers:d(),body:JSON.stringify(t)});return p(o)}async function v(e,t){let o=d();delete o["Content-Type"];let r=await fetch(m(e),{method:"POST",headers:o,body:t});return p(r)}function k(){return!!h}function _(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}import{z as G}from"zod";var W="Private to the owner: do not save this to memory, reports, shared notes or any other person's chat. Kynver gives no medical advice.";function a(e,t,o,r){return{name:e,description:t,inputSchemaZod:o,execute:async s=>{if(!k())return _();try{let i=await r(s);return JSON.stringify({privacy:W,...i&&typeof i=="object"?i:{result:i}})}catch(i){return JSON.stringify({error:i instanceof Error?i.message:String(i)})}}}}var w=e=>G.string().min(1).max(64).describe(e);var Q={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".webp":"image/webp"},S=8*1024*1024,A=!1;function N(){A=!0}var R=c.object({path:c.string().min(1).max(1024).optional().describe("Local image file (only when this server runs on the owner's own computer)"),base64:c.string().max(12e6).optional().describe("Image bytes, base64"),mediaType:c.enum(["image/jpeg","image/png","image/webp"]).optional()}).strict();async function P(e){if(e.path){if(!A)throw new Error("Send the photo as base64; file paths work only when Meds runs on your own computer");let o=Q[Z(e.path).toLowerCase()];if(!o)throw new Error("Use a JPEG, PNG or WebP photo");let r=await X(e.path);if(r.byteLength>S)throw new Error("Photo is too large");return new Blob([new Uint8Array(r)],{type:o})}if(!e.base64||!e.mediaType)throw new Error("Give the photo as a path or as base64 with its mediaType");let t=Buffer.from(e.base64,"base64");if(t.byteLength>S)throw new Error("Photo is too large");return new Blob([new Uint8Array(t)],{type:e.mediaType})}var K=a("meds_read_label","Read a pill-bottle label photo (front, optional back) into a DRAFT: name, strength, dose instructions, quantity, refills, pharmacy, Rx number, prescriber, fill date, suggested reminder times, and which fields it is unsure of. Nothing is saved and the photo is not kept. Show the draft to the owner, fix what they correct, then meds_add; the owner still confirms it in the app.",c.object({front:R,back:R.optional()}).strict(),async({front:e,back:t})=>{let o=new FormData;return o.append("front",await P(e),"front"),t&&o.append("back",await P(t),"back"),v("/api/meds/read-label",o)});import{McpServer as oe}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as n}from"zod";var ee=24*36e5,te=n.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/,"Use HH:MM (24h)"),L=a("meds_list","The owner's own medicine list and their doses for a window (default: the last and next 24 hours): each dose's time and status (due / taken / skipped / missed), refill warnings, and which medicines are paused, stopped or still waiting for the owner to confirm. Only the caller's own workspace.",n.object({hoursBack:n.number().int().min(0).max(744).optional().describe("Hours of past doses to include (default 24)"),hoursAhead:n.number().int().min(0).max(168).optional().describe("Hours of upcoming doses to include (default 24)")}).strict(),async e=>{let t=Date.now(),o=t-(e.hoursBack??24)*36e5,r=t+Math.max(1,e.hoursAhead??24)*36e5;return x(`/api/meds?from=${o}&to=${Math.min(r,o+62*ee)}`)}),I=a("meds_add","Add a medicine for the owner, usually from meds_read_label. It is saved as PENDING: no reminders start until the owner opens Meds in the Kynver app and confirms the details and times. Never guess a dose or schedule: leave unknown fields out and only pass reminder times the owner told you.",n.object({name:n.string().trim().min(1).max(120),strength:n.string().max(60).optional(),form:n.string().max(40).optional(),instructions:n.string().max(400).optional().describe("The label's dose instructions as printed"),doseAmount:n.string().max(60).optional().describe('What one dose is, e.g. "1 tablet"'),perDose:n.number().min(0).max(100).optional().describe("Units one dose uses (for the count-down)"),times:n.array(te).max(8).optional().describe("Reminder times the owner chose, local HH:MM"),asNeeded:n.boolean().optional(),quantity:n.number().int().min(0).max(1e5).optional(),refills:n.number().int().min(0).max(1e5).optional(),pharmacy:n.string().max(120).optional(),rxNumber:n.string().max(60).optional(),prescriber:n.string().max(120).optional(),fillDate:n.string().max(20).optional(),notes:n.string().max(500).optional(),timezone:n.string().min(1).max(64).describe("The owner's IANA time zone, e.g. America/Los_Angeles")}).strict(),async({timezone:e,...t})=>({...await E("/api/meds",{details:{times:[],...t},timezone:e,keepPhoto:!1}),next:"Saved as pending. Ask the owner to open Meds in the Kynver app to confirm it; reminders start after that."})),M=a("meds_log_dose","Mark one or more of the owner's doses (ids from meds_list) as taken, skipped, snoozed 10 minutes, or undo a mark. Only when the owner says so.",n.object({doseIds:n.array(w("Dose id from meds_list")).min(1).max(20),action:n.enum(["taken","skip","snooze","undo"])}).strict(),async e=>f("/api/meds/doses",e)),C=a("meds_set_status","Pause, stop or resume one of the owner's confirmed medicines. Paused and stopped medicines get no reminders. A medicine still waiting for the owner's confirmation can only be confirmed by the owner in the app.",n.object({medicineId:w("Medicine id from meds_list"),status:n.enum(["active","paused","stopped"])}).strict(),async({medicineId:e,status:t})=>f(`/api/meds/${encodeURIComponent(e)}`,{status:t}));var O=[L,K,I,M,C];var ne=u(import.meta.url);function re(e){return{content:[{type:"text",text:e}]}}function V(){let e=new oe({name:"kynver-mcp-meds",version:ne},{capabilities:{tools:{}}});for(let t of O)e.registerTool(t.name,{description:t.description,inputSchema:t.inputSchemaZod},async o=>re(await t.execute(o)));return e}T(process.argv.slice(2),import.meta.url,"kynver-mcp-meds");N();var ie=V(),ae=new se;await ie.connect(ae);
package/dist/sse.js ADDED
@@ -0,0 +1 @@
1
+ import{createServer as ye}from"node:http";import{randomUUID as we,timingSafeEqual as Se}from"node:crypto";import{SSEServerTransport as ve}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as Ee}from"@modelcontextprotocol/sdk/server/streamableHttp.js";var Te=1*1024*1024,be=["apiKey","secret","userId"],Ie=new Set(["1","true","yes","on"]);function xe(){let e=process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();return e!==void 0&&Ie.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 G(e,t){let r=Buffer.from(e),n=Buffer.from(t);return r.length!==n.length?!1:Se(r,n)}function F(e){return be.some(t=>e.searchParams.has(t))}function Re(){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 n=parseInt(r,10);return Number.isFinite(n)&&n>0?n:t}function Pe(){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 H(e){let t=v(e,"x-forwarded-for");return t?t.split(",")[0].trim():e.socket?.remoteAddress??"unknown"}function _e(e,t){let r=new Map;return{check(n){let u=Date.now(),i=r.get(n);return!i||u>=i.resetAt?(r.set(n,{count:1,resetAt:u+t}),!0):i.count>=e?!1:(i.count+=1,!0)}}}function A(e,t=Te){return new Promise((r,n)=>{let u=[],i=0;e.on("data",h=>{if(i+=h.length,i>t){e.destroy(),n(new Error("Request body too large"));return}u.push(h)}),e.on("end",()=>{try{r(JSON.parse(Buffer.concat(u).toString()))}catch(h){n(h)}}),e.on("error",n)})}function M(e){let t=v(e,"authorization");return t?/^Bearer\s+(.+)$/i.exec(t)?.[1]?.trim()??"":""}function K(e,t){let r=v(e,"x-kynver-user-id"),n=v(e,"x-kynver-service-secret");return!r||!t||!n||xe()||!G(n,t)?null:{userId:r,auth:{kind:"internal",userId:r}}}function X(e,t,r,n){if(r.kind==="apiKey"){let i=M(e);return i.length>0&&G(i,r.apiKey)}let u=K(e,n);return u!==null&&u.userId===r.userId&&u.auth.kind==="internal"}async function J(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 ke(e,t){return new URL(e.url??"/",`http://localhost:${t}`)}function c(e,t,r){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(r))}function W(e){let t=parseInt(process.env.PORT??String(e.defaultPort),10),r=Pe(),n=process.env.KYNVER_SERVICE_SECRET??"",u=Re(),i={},h={},V=x("MCP_SSE_MAX_SESSIONS",200),j=x("MCP_SSE_MAX_SESSIONS_PER_USER",20),D=x("MCP_SSE_IDLE_TIMEOUT_MS",300*1e3),fe=x("MCP_SSE_RATE_LIMIT_PER_MIN",120),T=_e(fe,60*1e3);setInterval(()=>{let s=Date.now();for(let[o,f]of Object.entries(i))s-f.lastActivity>D&&(delete i[o],f.transport.close().catch(()=>{}));for(let[o,f]of Object.entries(h))s-f.lastActivity>D&&(delete h[o],f.transport.close().catch(()=>{}))},60*1e3).unref?.();let z=()=>Object.keys(i).length+Object.keys(h).length,B=s=>Object.values(i).filter(o=>o.userId===s).length+Object.values(h).filter(o=>o.userId===s).length,Y=ye(async(s,o)=>{let f=ke(s,t),b=f.pathname;if(s.method==="GET"&&b==="/sse"){if(!T.check(`sse:${H(s)}`)){c(o,429,{error:"Too many requests. Slow down and retry."});return}if(F(f)){c(o,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let m=null,d=null,p=n?K(s,n):null;if(p)m=p.userId,d=p.auth;else{let y=M(s);y&&(m=await J(r,y),m&&(d={kind:"apiKey",apiKey:y}))}if(!m||!d){c(o,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(z()>=V){c(o,503,{error:"Server at capacity. Try again shortly."});return}if(B(m)>=j){c(o,429,{error:"Too many concurrent connections for this account."});return}console.log(`[MCP SSE] New connection (${e.label}, user: ${m.slice(0,8)}...)`);let g=new ve("/messages",o);i[g.sessionId]={transport:g,userId:m,auth:d,lastActivity:Date.now()},o.on("close",()=>{delete i[g.sessionId]}),await(await e.createMcpServer()).connect(g);return}if(s.method==="POST"&&b==="/messages"){let m=f.searchParams.get("sessionId"),d=m?i[m]:void 0;if(!d){c(o,400,{error:"No active SSE session for this sessionId"});return}if(!X(s,f,d.auth,n)){c(o,401,{error:"Unauthorized. Re-send the same credentials used to open the SSE session."});return}if(!T.check(`msg:${d.userId}`)){c(o,429,{error:"Too many requests. Slow down and retry."});return}d.lastActivity=Date.now();try{let p=await A(s);await e.requestContext.run({userId:d.userId,authKind:d.auth.kind,sessionCredential:d.auth.kind==="apiKey"?d.auth.apiKey:void 0},async()=>{await d.transport.handlePostMessage(s,o,p)})}catch(p){c(o,400,{error:p instanceof Error?p.message:"Invalid request body"})}return}if(b==="/mcp"){if(s.method!=="POST"&&s.method!=="GET"&&s.method!=="DELETE"){o.writeHead(405,{"Content-Type":"application/json",Allow:"GET, POST, DELETE"}),o.end(JSON.stringify({error:"Method not allowed"}));return}if(F(f)){c(o,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let m=v(s,"mcp-session-id");if(m){let l=h[m];if(!l){c(o,404,{error:"Unknown or expired MCP session. Re-initialize."});return}if(!X(s,f,l.auth,n)){c(o,401,{error:"Unauthorized. Re-send the same credentials used to initialize the MCP session."});return}if(!T.check(`msg:${l.userId}`)){c(o,429,{error:"Too many requests. Slow down and retry."});return}l.lastActivity=Date.now();try{let I=s.method==="POST"?await A(s):void 0;await e.requestContext.run({userId:l.userId},async()=>{await l.transport.handleRequest(s,o,I)})}catch(I){o.headersSent||c(o,400,{error:I instanceof Error?I.message:"Invalid request body"})}return}if(s.method!=="POST"){c(o,400,{error:"Missing mcp-session-id header. Open a session with an initialize POST first."});return}if(!T.check(`mcp:${H(s)}`)){c(o,429,{error:"Too many requests. Slow down and retry."});return}let d=null,p=null,g=n?K(s,n):null;if(g)d=g.userId,p=g.auth;else{let l=M(s);l&&(d=await J(r,l),d&&(p={kind:"apiKey",apiKey:l}))}if(!d||!p){c(o,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(z()>=V){c(o,503,{error:"Server at capacity. Try again shortly."});return}if(B(d)>=j){c(o,429,{error:"Too many concurrent connections for this account."});return}let k;try{k=await A(s)}catch(l){c(o,400,{error:l instanceof Error?l.message:"Invalid request body"});return}let y=d,ge=p,S=new Ee({sessionIdGenerator:()=>we(),onsessioninitialized:l=>{h[l]={transport:S,userId:y,auth:ge,lastActivity:Date.now()},console.log(`[MCP HTTP] New session (${e.label}, user: ${y.slice(0,8)}...)`)}});S.onclose=()=>{S.sessionId&&delete h[S.sessionId]},await(await e.createMcpServer()).connect(S);try{await e.requestContext.run({userId:y},async()=>{await S.handleRequest(s,o,k)})}catch(l){o.headersSent||c(o,400,{error:l instanceof Error?l.message:"Invalid request"})}return}if(s.method==="GET"&&b==="/health"){o.writeHead(200,{"Content-Type":"text/plain"}),o.end("ok");return}c(o,404,{error:"Not found"})});return Y.listen(t,u,()=>{console.log(`[MCP SSE] ${e.label} listening on ${u}:${t}`)}),Y}import{McpServer as He}from"@modelcontextprotocol/sdk/server/mcp.js";import{readFile as Le}from"node:fs/promises";import{extname as Oe}from"node:path";import{z as E}from"zod";import{AsyncLocalStorage as Ae}from"node:async_hooks";var C=new Ae,N=process.env.KYNVER_API_KEY??"";function Me(){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 Ke(){return Me()}function Ne(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 R(e){Ne(e);let t=e.startsWith("/")?e:`/${e}`,r=t.startsWith("/api")?t:`/api${t}`;return`${Ke()}${r}`}function P(){let e={"Content-Type":"application/json"};N&&(e.Authorization=`Bearer ${N}`);let t=C.getStore(),r=t?.userId??process.env.KYNVER_USER_ID?.trim(),n=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 u=process.env.KYNVER_RUNNER_TOKEN?.trim();if(u){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"]=u,e}if(!n)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"]=n}return e}async function _(e){let t=await e.text();if(!e.ok){let r=t;try{let n=JSON.parse(t);r=n.error??n.message??t}catch{}throw new Error(r||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function Z(e){let t=await fetch(R(e),{method:"GET",headers:P()});return _(t)}async function Q(e,t){let r=await fetch(R(e),{method:"POST",headers:P(),body:JSON.stringify(t)});return _(r)}async function U(e,t){let r=await fetch(R(e),{method:"PATCH",headers:P(),body:JSON.stringify(t)});return _(r)}async function q(e,t){let r=P();delete r["Content-Type"];let n=await fetch(R(e),{method:"POST",headers:r,body:t});return _(n)}function ee(){return!!N}function te(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}import{z as Ce}from"zod";var Ue="Private to the owner: do not save this to memory, reports, shared notes or any other person's chat. Kynver gives no medical advice.";function w(e,t,r,n){return{name:e,description:t,inputSchemaZod:r,execute:async u=>{if(!ee())return te();try{let i=await n(u);return JSON.stringify({privacy:Ue,...i&&typeof i=="object"?i:{result:i}})}catch(i){return JSON.stringify({error:i instanceof Error?i.message:String(i)})}}}}var O=e=>Ce.string().min(1).max(64).describe(e);var $e={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".webp":"image/webp"},re=8*1024*1024,Ve=!1;var ne=E.object({path:E.string().min(1).max(1024).optional().describe("Local image file (only when this server runs on the owner's own computer)"),base64:E.string().max(12e6).optional().describe("Image bytes, base64"),mediaType:E.enum(["image/jpeg","image/png","image/webp"]).optional()}).strict();async function oe(e){if(e.path){if(!Ve)throw new Error("Send the photo as base64; file paths work only when Meds runs on your own computer");let r=$e[Oe(e.path).toLowerCase()];if(!r)throw new Error("Use a JPEG, PNG or WebP photo");let n=await Le(e.path);if(n.byteLength>re)throw new Error("Photo is too large");return new Blob([new Uint8Array(n)],{type:r})}if(!e.base64||!e.mediaType)throw new Error("Give the photo as a path or as base64 with its mediaType");let t=Buffer.from(e.base64,"base64");if(t.byteLength>re)throw new Error("Photo is too large");return new Blob([new Uint8Array(t)],{type:e.mediaType})}var se=w("meds_read_label","Read a pill-bottle label photo (front, optional back) into a DRAFT: name, strength, dose instructions, quantity, refills, pharmacy, Rx number, prescriber, fill date, suggested reminder times, and which fields it is unsure of. Nothing is saved and the photo is not kept. Show the draft to the owner, fix what they correct, then meds_add; the owner still confirms it in the app.",E.object({front:ne,back:ne.optional()}).strict(),async({front:e,back:t})=>{let r=new FormData;return r.append("front",await oe(e),"front"),t&&r.append("back",await oe(t),"back"),q("/api/meds/read-label",r)});import{z as a}from"zod";var je=24*36e5,De=a.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/,"Use HH:MM (24h)"),ie=w("meds_list","The owner's own medicine list and their doses for a window (default: the last and next 24 hours): each dose's time and status (due / taken / skipped / missed), refill warnings, and which medicines are paused, stopped or still waiting for the owner to confirm. Only the caller's own workspace.",a.object({hoursBack:a.number().int().min(0).max(744).optional().describe("Hours of past doses to include (default 24)"),hoursAhead:a.number().int().min(0).max(168).optional().describe("Hours of upcoming doses to include (default 24)")}).strict(),async e=>{let t=Date.now(),r=t-(e.hoursBack??24)*36e5,n=t+Math.max(1,e.hoursAhead??24)*36e5;return Z(`/api/meds?from=${r}&to=${Math.min(n,r+62*je)}`)}),ae=w("meds_add","Add a medicine for the owner, usually from meds_read_label. It is saved as PENDING: no reminders start until the owner opens Meds in the Kynver app and confirms the details and times. Never guess a dose or schedule: leave unknown fields out and only pass reminder times the owner told you.",a.object({name:a.string().trim().min(1).max(120),strength:a.string().max(60).optional(),form:a.string().max(40).optional(),instructions:a.string().max(400).optional().describe("The label's dose instructions as printed"),doseAmount:a.string().max(60).optional().describe('What one dose is, e.g. "1 tablet"'),perDose:a.number().min(0).max(100).optional().describe("Units one dose uses (for the count-down)"),times:a.array(De).max(8).optional().describe("Reminder times the owner chose, local HH:MM"),asNeeded:a.boolean().optional(),quantity:a.number().int().min(0).max(1e5).optional(),refills:a.number().int().min(0).max(1e5).optional(),pharmacy:a.string().max(120).optional(),rxNumber:a.string().max(60).optional(),prescriber:a.string().max(120).optional(),fillDate:a.string().max(20).optional(),notes:a.string().max(500).optional(),timezone:a.string().min(1).max(64).describe("The owner's IANA time zone, e.g. America/Los_Angeles")}).strict(),async({timezone:e,...t})=>({...await Q("/api/meds",{details:{times:[],...t},timezone:e,keepPhoto:!1}),next:"Saved as pending. Ask the owner to open Meds in the Kynver app to confirm it; reminders start after that."})),ce=w("meds_log_dose","Mark one or more of the owner's doses (ids from meds_list) as taken, skipped, snoozed 10 minutes, or undo a mark. Only when the owner says so.",a.object({doseIds:a.array(O("Dose id from meds_list")).min(1).max(20),action:a.enum(["taken","skip","snooze","undo"])}).strict(),async e=>U("/api/meds/doses",e)),ue=w("meds_set_status","Pause, stop or resume one of the owner's confirmed medicines. Paused and stopped medicines get no reminders. A medicine still waiting for the owner's confirmation can only be confirmed by the owner in the app.",a.object({medicineId:O("Medicine id from meds_list"),status:a.enum(["active","paused","stopped"])}).strict(),async({medicineId:e,status:t})=>U(`/api/meds/${encodeURIComponent(e)}`,{status:t}));var de=[ie,se,ae,ce,ue];import{existsSync as ze,readFileSync as Be}from"node:fs";import{dirname as $,join as me}from"node:path";import{fileURLToPath as le}from"node:url";function Ye(e){let t=$(le(e));for(let r=0;r<6;r+=1){if(ze(me(t,"package.json")))return t;let n=$(t);if(n===t)break;t=n}throw new Error(`package.json not found above ${$(le(e))}`)}function Fe(e,t){let r=me(Ye(e),"package.json"),u=JSON.parse(Be(r,"utf8"))[t];if(typeof u!="string"||!u.trim())throw new Error(`Missing package.json ${t} at ${r}`);return u}function pe(e=import.meta.url){return Fe(e,"version")}var Xe=pe(import.meta.url);function Je(e){return{content:[{type:"text",text:e}]}}function he(){let e=new He({name:"kynver-mcp-meds",version:Xe},{capabilities:{tools:{}}});for(let t of de)e.registerTool(t.name,{description:t.description,inputSchema:t.inputSchemaZod},async r=>Je(await t.execute(r)));return e}W({label:"Meds server",defaultPort:3113,createMcpServer:he,requestContext:C});
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@kynver-app/mcp-meds",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "dist",
6
+ "!dist/**/__tests__/**",
7
+ "!dist/**/*.map",
8
+ "!dist/**/*.d.ts",
9
+ "!dist/**/*.d.ts.map"
10
+ ],
11
+ "description": "Kynver Meds MCP server: your own private medicine list and today's doses. Read a bottle label into a draft, add it for you to confirm in the app, log a dose as taken or skipped, pause or stop a medicine. Answers only your own workspace; no medical advice.",
12
+ "type": "module",
13
+ "bin": {
14
+ "kynver-mcp-meds": "dist/index.js"
15
+ },
16
+ "scripts": {
17
+ "prebuild": "node ../../scripts/sync-mcp-sse-server.mjs",
18
+ "build": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc --noEmit && node ../../scripts/esbuild-mcp-package.mjs",
19
+ "dev": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc --watch",
20
+ "start": "node dist/index.js",
21
+ "start:sse": "node dist/sse.js",
22
+ "prepublishOnly": "npm run build && node ../../scripts/npm-publish-guard.mjs"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "zod": "^4.6.6"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/Totalsolutionsync/kynver-feedback.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/Totalsolutionsync/kynver-feedback/issues"
41
+ },
42
+ "homepage": "https://www.kynver.com"
43
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.