@atoms-tech/atoms-mcp 0.3.4 → 0.3.5

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.cjs CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var e=require("fs/promises"),t=require("os"),r=require("path"),n=require("@supabase/supabase-js"),i=require("@modelcontextprotocol/ext-apps/server"),a=require("url"),s=require("@modelcontextprotocol/sdk/server/mcp.js"),o=require("zod"),c=require("crypto"),d=require("http"),l=require("@modelcontextprotocol/sdk/server/stdio.js"),m="undefined"!=typeof document?document.currentScript:null;function p(e){return e&&e.__esModule?e:{default:e}}var u,h,_,g,f,y=p(e),w=p(r),b=Object.defineProperty,v=Object.getOwnPropertyNames,j=(e,t)=>function(){return e&&(t=(0,e[v(e)[0]])(e=0)),t},x=(e,t)=>{for(var r in t)b(e,r,{get:t[r],enumerable:!0})},S=j({"src/middleware/audit.ts"(){}}),I=j({"src/middleware/rate-limiter.ts"(){u=60,h=6e4,_=new Map}}),k={};async function E(){try{const t=await e.readFile(f,"utf-8"),r=JSON.parse(t);return r.access_token&&r.refresh_token?r:null}catch{return null}}async function T(t){await e.mkdir(g,{recursive:!0,mode:448}),await e.writeFile(f,JSON.stringify(t,null,2),{mode:384})}async function C(){const e=await E();return!!e&&e.expires_at>Date.now()+6e4}async function H(){try{await e.unlink(f)}catch{}}function U(){return f}x(k,{clearCredentials:()=>H,getCredentialsPath:()=>U,hasValidCredentials:()=>C,readCredentials:()=>E,writeCredentials:()=>T});var O,q,z,D=j({"src/auth/token-store.ts"(){g=r.join(t.homedir(),".atoms"),f=r.join(g,"credentials.json")}}),M=j({"src/config.ts"(){O="https://gmebjyhomsbvhrxffzre.supabase.co",q="sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH",z=process.env.ATOMS_APP_URL??"https://x.atoms.tech"}}),R={};async function P(){const e=process.env.ATOMS_ACCESS_TOKEN;if(e){const t=A(e);return{access_token:e,refresh_token:"",user_id:t.sub,email:t.email??""}}const t=await E();if(!t)throw new Error("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' to connect your ATOMS account.");if(t.expires_at>Date.now()+6e4){const e=A(t.access_token);return{access_token:t.access_token,refresh_token:t.refresh_token,user_id:e.sub,email:e.email??t.user_email??""}}process.stderr.write("[atoms-mcp] Refreshing access token...\n");const r=n.createClient(O,q),{data:i,error:a}=await r.auth.refreshSession({refresh_token:t.refresh_token});if(a||!i.session)throw new Error(`Token refresh failed: ${a?.message??"No session returned"}. Re-run 'npx @atoms-tech/atoms-mcp login'.`);const s=i.session;return await T({access_token:s.access_token,refresh_token:s.refresh_token,expires_at:Date.now()+1e3*(s.expires_in??3600),user_email:s.user?.email}),{access_token:s.access_token,refresh_token:s.refresh_token,user_id:s.user?.id??"",email:s.user?.email??""}}function A(e){const t=e.split(".");if(3!==t.length)throw new Error("Invalid JWT format");const r=t[1].replace(/-/g,"+").replace(/_/g,"/").padEnd(t[1].length+(4-t[1].length%4)%4,"="),n=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));if(!n.sub)throw new Error("JWT missing 'sub' claim");return n}x(R,{getValidToken:()=>P});var N,$,F,W,L=j({"src/auth/refresh.ts"(){D(),M()}});async function J(){if(N&&W>Date.now()+6e4)return N;N&&process.stderr.write("[atoms-mcp] Token expiring, refreshing client...\n");const{access_token:e,user_id:t,email:r}=await P(),i=e.split(".");if(3===i.length)try{const e=JSON.parse(Buffer.from(i[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString());W=1e3*(e.exp??0)}catch{W=Date.now()+36e5}const a=n.createClient(O,q,{global:{headers:{Authorization:`Bearer ${e}`}}});return $=t,F=r,N=a}function B(){if(!$)throw new Error("Not authenticated. Call getClient() first.");return $}async function V(e,t){const r=B(),{data:n,error:i}=await e.from("projects").select("org_id").eq("id",t).maybeSingle();if(i||!n)throw new Error(`Project '${t}' not found`);const{data:a,error:s}=await e.from("org_members").select("role").eq("user_id",r).eq("org_id",n.org_id).maybeSingle();if(s||!a){const{data:t}=await e.rpc("is_platform_admin");if(!0===t)return"admin";throw new Error("Not a member of this project's organization")}if("viewer"===a.role)throw new Error("VIEWER_ROLE");return a.role}var G,Q=j({"src/db/client.ts"(){L(),M(),N=null,$=null,F=null,W=0}});function Y(e,t){return{status:"success",data:e,...t?{meta:t}:{}}}function K(e,t,r){return{total_count:e,limit:t,offset:r,has_more:r+t<e}}function X(e,t){return{status:"error",message:e,next_steps:t}}function Z(e,t){return X(`${e} '${t}' not found`,[`Verify the ${e.toLowerCase()} ID is correct`,"Use atoms_list_items or atoms_search to find valid IDs"])}function ee(e){return X(`${e} role cannot modify project data`,["Contact your org admin to upgrade your role to editor","Use read-only tools (atoms_list_items, atoms_get_item, atoms_search) instead"])}function te(e){return X(`Validation error: ${e}`,["Check the parameter types and constraints in the tool description","Ensure all required parameters are provided"])}function re(){return X("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' first.",["Run: npx @atoms-tech/atoms-mcp login","Or set ATOMS_ACCESS_TOKEN environment variable"])}function ne(e){return X(`Database error: ${e}`,["This may be a temporary issue — try again","If the error persists, check that the project_id is valid"])}function ie(e){const t=JSON.stringify(e,null,2);if(t.length>G&&"success"===e.status&&Array.isArray(e.data)){const t=Math.max(1,Math.floor(e.data.length/2)),r={...e,data:e.data.slice(0,t),meta:{...e.meta,truncated:!0,truncation_message:`Response truncated from ${e.data.length} to ${t} items. Use 'offset' parameter or add filters to see more results.`}};return{content:[{type:"text",text:JSON.stringify(r,null,2)}],structuredContent:r}}return{content:[{type:"text",text:t}],structuredContent:e}}function ae(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}],structuredContent:e,isError:!0}}var se,oe=j({"src/tools/_base.ts"(){G=25e3}});function ce(e,t){const r=`ui://${t.toolName}/${t.htmlFile}`,n=function(e,t){const r=a.fileURLToPath("undefined"==typeof document?require("url").pathToFileURL(__filename).href:m&&"SCRIPT"===m.tagName.toUpperCase()&&m.src||new URL("index.cjs",document.baseURI).href),n=w.default.dirname(r);return w.default.resolve(n,"apps","src","apps",e,t)}(t.appName,t.htmlFile),s=[...se.resourceDomains,...t.extraResourceDomains??[]];i.registerAppTool(e,t.toolName,{title:t.title,description:t.description,inputSchema:t.inputSchema,annotations:t.annotations,_meta:{ui:{resourceUri:r}}},t.handler),i.registerAppResource(e,`${t.title} UI`,r,{mimeType:i.RESOURCE_MIME_TYPE},async()=>{const e=await y.default.readFile(n,"utf-8");return{contents:[{uri:r,mimeType:i.RESOURCE_MIME_TYPE,text:e,_meta:{ui:{csp:{resourceDomains:s}}}}]}})}var de=j({"src/apps/register.ts"(){se={resourceDomains:["fonts.googleapis.com","fonts.gstatic.com"]}}});async function le(e,t,r){const{data:n,error:i}=await e.from("items").select("*").eq("id",r).eq("project_id",t).is("deleted_at",null).maybeSingle();if(i)throw new Error(i.message);return n}async function me(e,t,r){const n="requirement"===r?"REQ":"test-case"===r?"TC":"note"===r?"NOTE":"TABLE",{data:i,error:a}=await e.rpc("next_item_id",{p_project_id:t,p_prefix:n});if(a)throw new Error(`ID generation failed: ${a.message}`);return i}var pe=j({"src/db/queries.ts"(){}}),ue={};async function he(){try{const e=await J(),t=await async function(e){const{data:t,error:r}=await e.from("projects").select("id, name, description, org_id, created_at, organizations(name)").is("deleted_at",null).order("created_at",{ascending:!1});if(r)throw new Error(r.message);return(t??[]).map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.organizations?.name??"Unknown",created_at:e.created_at}))}(e);return ie(Y(t.map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.org_name,created_at:e.created_at}))))}catch(e){return ae(ne(e instanceof Error?e.message:"Unknown error"))}}x(ue,{listProjectsHandler:()=>he});var _e=j({"src/tools/list-projects.ts"(){Q(),pe(),oe()}}),ge={};async function fe(e){try{const t=await J(),{items:r,totalCount:n}=await async function(e,t,r){let n=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).order("created_at",{ascending:!0}).range(r.offset,r.offset+r.limit-1);r.type&&(n=n.eq("type",r.type));const{data:i,error:a,count:s}=await n;if(a)throw new Error(a.message);let o=i??[];return r.domain&&(o=o.filter(e=>(e.data?.tags?.domains??[]).includes(r.domain))),r.level&&(o=o.filter(e=>e.data?.tags?.level===r.level)),{items:o,totalCount:r.domain||r.level?o.length:s??0}}(t,e.project_id,{type:e.type,domain:e.domain,level:e.level,limit:e.limit,offset:e.offset});return ie(Y(r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),K(n,e.limit,e.offset)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(ge,{listItemsHandler:()=>fe});var ye=j({"src/tools/list-items.ts"(){Q(),pe(),oe()}}),we={};async function be(e){try{const t=await J(),r=await le(t,e.project_id,e.item_id);if(!r)return ae(Z("Item",e.item_id));const n=await async function(e,t,r){const{data:n,error:i}=await e.from("item_relationships").select("from_id, to_id, type").eq("project_id",t).or(`from_id.eq.${r},to_id.eq.${r}`);if(i)throw new Error(i.message);return n??[]}(t,e.project_id,e.item_id),i={parents:[],children:[],related:[],verified_by:[],verifies:[]};for(const t of n)if(t.from_id===e.item_id)t.type in i&&i[t.type].push(t.to_id);else{const e={parent:"children",child:"parents",verified_by:"verifies",verifies:"verified_by",related:"related"}[t.type]??t.type;e in i&&i[e].push(t.from_id)}let a=[];if("test-case"===r.type){const{data:r,error:n}=await t.from("test_results").select("result, run_by, run_at, note").eq("item_id",e.item_id).eq("project_id",e.project_id).order("run_at",{ascending:!1}).limit(20);!n&&r&&(a=r)}return ie(Y({id:r.id,type:r.type,title:r.title,summary:r.data?.summary,body:r.data?.body,tags:r.data?.tags??{domains:[]},ownership:r.data?.ownership??{primary:null,additional:[]},relationships:i,links:r.data?.links??[],status:r.data?.status,..."test-case"===r.type?{latest_result:a[0]?.result??"not-run",test_results:a}:{},metadata:{created_at:r.created_at,created_by:r.created_by,updated_at:r.updated_at,updated_by:r.updated_by,source_id:r.data?.metadata?.source_id}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(we,{getItemHandler:()=>be});var ve=j({"src/tools/get-item.ts"(){Q(),pe(),oe()}}),je={};async function xe(e){try{const t=await J(),{items:r,totalCount:n}=await async function(e,t,r,n){const i=r.trim();if(!i)return{items:[],totalCount:0};let a=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).or(`title.ilike.%${i}%,data->>body.ilike.%${i}%,data->>summary.ilike.%${i}%`).limit(n.limit);n.type&&(a=a.eq("type",n.type));const{data:s,error:o,count:c}=await a;if(o)throw new Error(o.message);return{items:s??[],totalCount:c??0}}(t,e.project_id,e.query,{type:e.type,limit:e.limit});return ie(Y(r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),K(n,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(je,{searchHandler:()=>xe});var Se=j({"src/tools/search.ts"(){Q(),pe(),oe()}}),Ie={};async function ke(e){try{const t=await J(),{covered:r,uncovered:n,total:i}=await async function(e,t,r){const{data:n,error:i}=await e.from("items").select("*").eq("project_id",t).eq("type","requirement").is("deleted_at",null);if(i)throw new Error(i.message);const a=(n??[]).filter(e=>!(r.domain&&!(e.data?.tags?.domains??[]).includes(r.domain)||r.level&&e.data?.tags?.level!==r.level)),{data:s,error:o}=await e.from("item_relationships").select("from_id").eq("project_id",t).eq("type","verified_by");if(o)throw new Error(o.message);const c=new Set((s??[]).map(e=>e.from_id));return{covered:a.filter(e=>c.has(e.id)),uncovered:a.filter(e=>!c.has(e.id)),total:a.length}}(t,e.project_id,{domain:e.domain,level:e.level}),a=n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level}));return ie(Y({covered:r.length,uncovered:n.length,total:i,coverage_percent:i>0?Math.round(r.length/i*1e3)/10:100,uncovered_items:a}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Ie,{getCoverageHandler:()=>ke});var Ee=j({"src/tools/get-coverage.ts"(){Q(),pe(),oe()}}),Te={};async function Ce(e){try{const t=await J(),r=await async function(e,t,r,n){const{data:i,error:a}=await e.from("change_history").select("*").eq("item_id",r).eq("project_id",t).order("changed_at",{ascending:!1}).limit(n);if(a)throw new Error(a.message);return i??[]}(t,e.project_id,e.item_id,e.limit),n=r.map(e=>{const t=[],r=e.old_data,n=e.new_data;if(r&&n){const e=new Set([...Object.keys(r),...Object.keys(n)]);for(const i of e)JSON.stringify(r[i])!==JSON.stringify(n[i])&&t.push(i)}else n?t.push("(created)"):r&&t.push("(deleted)");return{id:e.id,event_type:e.event_type,changed_by:e.changed_by??null,changed_at:e.changed_at,actor:e.actor??"user",session_id:e.session_id??null,fields_changed:t}});return ie(Y(n,K(n.length,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Te,{getHistoryHandler:()=>Ce});var He=j({"src/tools/get-history.ts"(){Q(),pe(),oe()}}),Ue={};async function Oe(e){try{const t=await J(),{data:r,error:n}=await t.from("items").select("id, title, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(n)throw new Error(n.message);if(!r||0===r.length)return ie(Y({mermaid:"graph TD\n empty[No items in project]"}));const{data:i,error:a}=await t.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(a)throw new Error(a.message);const s=new Map;for(const e of r)s.set(e.id,e);const o=["graph TD"],c=new Set,d=new Set,l=(new Set((i??[]).filter(e=>"child"===e.type).map(e=>e.to_id)),new Set((i??[]).filter(e=>"parent"===e.type).map(e=>e.from_id)));let m;m=e.root_item_id?[e.root_item_id]:r.filter(e=>!l.has(e.id)).filter(t=>!(!e.include_tests&&"test-case"===t.type)).map(e=>e.id);const p=m.map(e=>({id:e,depth:0}));for(;p.length>0;){const{id:t,depth:r}=p.shift();if(c.has(t)||r>e.depth)continue;c.add(t);const n=s.get(t);if(!n)continue;if(!e.include_tests&&"test-case"===n.type)continue;const a=qe(n.title),l="requirement"===n.type?`["${a}"]`:"test-case"===n.type?`(["${a}"])`:`("${a}")`;o.push(` ${t}${l}`);for(const n of i??[]){if(n.from_id!==t)continue;const i=s.get(n.to_id);if(!i)continue;if(!e.include_tests&&"test-case"===i.type)continue;const a=`${n.from_id}-${n.type}-${n.to_id}`;d.has(a)||(d.add(a),"child"===n.type?o.push(` ${t} --\x3e ${n.to_id}`):"verified_by"===n.type?o.push(` ${t} -.->|verified_by| ${n.to_id}`):"related"===n.type&&o.push(` ${t} -.- ${n.to_id}`),!c.has(n.to_id)&&r+1<=e.depth&&p.push({id:n.to_id,depth:r+1}))}}return ie(Y({mermaid:o.join("\n"),node_count:c.size,edge_count:d.size}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}function qe(e){return e.replace(/"/g,"'").replace(/[[\]{}()]/g,"").substring(0,50)}x(Ue,{exportMermaidHandler:()=>Oe});var ze=j({"src/tools/export-mermaid.ts"(){Q(),oe()}}),De={};async function Me(e){try{const t=await J(),r=B();try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const n=await me(t,e.project_id,e.type),i={id:n,type:e.type,title:e.title,summary:e.summary,body:e.body,tags:{domains:e.domains??[],level:e.level},ownership:{primary:null,additional:[]},relationships:{parents:e.parent_ids??[],children:[],related:[]},links:[],metadata:{created_at:(new Date).toISOString(),created_by:r,updated_at:(new Date).toISOString(),updated_by:r}},{error:a}=await t.from("items").insert({id:n,project_id:e.project_id,type:e.type,title:e.title,data:i,created_by:r,updated_by:r}).select().single();if(a)throw new Error(a.message);if(e.parent_ids&&e.parent_ids.length>0){const r=e.parent_ids.map(t=>({from_id:n,to_id:t,type:"parent",project_id:e.project_id}));await t.from("item_relationships").insert(r).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: relationship sync failed: ${e.message}\n`)})}return await t.from("change_history").insert({item_id:n,project_id:e.project_id,changed_by:r,event_type:"created",old_data:null,new_data:i,actor:"mcp_claude",session_id:ut()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ie(Y({id:n,type:e.type,title:e.title,project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(De,{createItemHandler:()=>Me});var Re=j({"src/tools/create-item.ts"(){Q(),pe(),gt(),oe()}}),Pe={};async function Ae(e){try{const t=await J(),r=B();try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const n=await le(t,e.project_id,e.item_id);if(!n)return ae(Z("Item",e.item_id));const i={...n.data},a={...n.data};void 0!==e.title&&(a.title=e.title),void 0!==e.body&&(a.body=e.body),void 0!==e.summary&&(a.summary=e.summary),void 0!==e.domains&&(a.tags={...a.tags,domains:e.domains}),void 0!==e.level&&(a.tags={...a.tags,level:e.level}),void 0!==e.status&&(a.status=e.status),a.metadata={...a.metadata,updated_at:(new Date).toISOString(),updated_by:r};const{error:s}=await t.from("items").update({title:e.title??n.title,data:a,updated_by:r}).eq("id",e.item_id).eq("project_id",e.project_id);if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"updated",old_data:i,new_data:a,actor:"mcp_claude",session_id:ut()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ie(Y({id:e.item_id,title:a.title,type:n.type,updated_fields:Object.keys(e).filter(t=>"project_id"!==t&&"item_id"!==t&&void 0!==e[t])}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Pe,{updateItemHandler:()=>Ae});var Ne=j({"src/tools/update-item.ts"(){Q(),pe(),gt(),oe()}}),$e={};async function Fe(e){try{const t=await J(),r=B();try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const n=await le(t,e.project_id,e.item_id);if(!n)return ae(Z("Item",e.item_id));const{error:i}=await t.from("items").update({deleted_at:(new Date).toISOString()}).eq("id",e.item_id).eq("project_id",e.project_id);if(i)throw new Error(i.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"deleted",old_data:n.data,new_data:null,actor:"mcp_claude",session_id:ut()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ie(Y({id:e.item_id,title:n.title,type:n.type,deleted:!0,message:`Item ${e.item_id} soft-deleted. It can still be found in audit logs.`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x($e,{deleteItemHandler:()=>Fe});var We=j({"src/tools/delete-item.ts"(){Q(),pe(),gt(),oe()}}),Le={};async function Je(e){try{const t=await J(),r=B();if(e.from_id===e.to_id)return ae(te("Cannot create a relationship between an item and itself"));try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const n=await le(t,e.project_id,e.from_id);if(!n)return ae(Z("Item",e.from_id));if(!await le(t,e.project_id,e.to_id))return ae(Z("Item",e.to_id));if("add"===e.action){const{data:i}=await t.from("item_relationships").select("from_id").eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id).maybeSingle();if(!i){const{error:r}=await t.from("item_relationships").insert({from_id:e.from_id,to_id:e.to_id,type:e.type,project_id:e.project_id});if(r)throw new Error(r.message)}const a={...n.data},s="verified_by"===e.type||"verifies"===e.type?e.type:e.type+"s"=="parents"||"parent"===e.type?"parents":"child"===e.type?"children":"related";if(a.relationships){const t=a.relationships[s]??[];t.includes(e.to_id)||(a.relationships[s]=[...t,e.to_id])}await t.from("items").update({data:a,updated_by:r}).eq("id",e.from_id).eq("project_id",e.project_id)}else{const{error:i}=await t.from("item_relationships").delete().eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id);if(i)throw new Error(i.message);const a={...n.data},s="parent"===e.type?"parents":"child"===e.type?"children":e.type;if(a.relationships){const t=a.relationships[s]??[];a.relationships[s]=t.filter(t=>t!==e.to_id)}await t.from("items").update({data:a,updated_by:r}).eq("id",e.from_id).eq("project_id",e.project_id)}return await t.from("change_history").insert({item_id:e.from_id,project_id:e.project_id,changed_by:r,event_type:"updated",old_data:null,new_data:{relationship_change:{action:e.action,type:e.type,from:e.from_id,to:e.to_id}},actor:"mcp_claude",session_id:ut()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ie(Y({action:e.action,type:e.type,from_id:e.from_id,to_id:e.to_id,message:`Relationship ${"add"===e.action?"added":"removed"}: ${e.from_id} --[${e.type}]--\x3e ${e.to_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Le,{linkItemsHandler:()=>Je});var Be=j({"src/tools/link-items.ts"(){Q(),pe(),gt(),oe()}}),Ve={};async function Ge(e){try{if(!e.items||0===e.items.length)return ae(te("items array must contain at least 1 item"));if(e.items.length>100)return ae(te(`Maximum 100 items per call (received ${e.items.length}). Split into multiple calls.`));const t=await J(),r=B();try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const n=(new Date).toISOString(),i=ut(),a=[],s=[],o=[];for(let r=0;r<e.items.length;r++){const n=e.items[r];try{const i=await me(t,e.project_id,n.type);o.push({itemId:i,input:n,index:r})}catch(e){s.push({index:r,title:n.title,error:`ID generation failed: ${e instanceof Error?e.message:String(e)}`})}}if(0===o.length)return ie(Y({created:0,items:[],errors:s}));const c=[],d=new Map;for(const{itemId:t,input:i}of o){const a={id:t,type:i.type,title:i.title,summary:i.summary,body:i.body,tags:{domains:i.domains??[],level:i.level},ownership:{primary:null,additional:[]},relationships:{parents:i.parent_id?[i.parent_id]:[],children:[],related:[]},links:[],metadata:{created_at:n,created_by:r,updated_at:n,updated_by:r}};d.set(t,a),c.push({id:t,project_id:e.project_id,type:i.type,title:i.title,data:a,created_by:r,updated_by:r})}const{error:l}=await t.from("items").insert(c);if(l)for(const{itemId:e,input:r,index:n}of o){const i=c.find(t=>t.id===e);if(!i)continue;const{error:o}=await t.from("items").insert(i);o?s.push({index:n,title:r.title,error:o.message}):a.push({id:e,title:r.title,type:r.type})}else for(const{itemId:e,input:t}of o)a.push({id:e,title:t.title,type:t.type});const m=o.filter(({input:e})=>e.parent_id).filter(({itemId:e})=>a.some(t=>t.id===e)).map(({itemId:t,input:r})=>({from_id:t,to_id:r.parent_id,type:"parent",project_id:e.project_id}));m.length>0&&await t.from("item_relationships").insert(m).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk relationship sync failed: ${e.message}\n`)});const p=a.map(t=>({item_id:t.id,project_id:e.project_id,changed_by:r,event_type:"created",old_data:null,new_data:d.get(t.id)??null,actor:"mcp_claude",session_id:i}));return p.length>0&&await t.from("change_history").insert(p).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk change_history log failed: ${e.message}\n`)}),ie(Y({created:a.length,items:a,errors:s.length>0?s:[],project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Ve,{bulkImportHandler:()=>Ge});var Qe=j({"src/tools/bulk-import.ts"(){Q(),pe(),gt(),oe()}}),Ye={};async function Ke(e){try{const t=await J(),r=B(),n=F??"";try{await V(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return ae(ee("Viewer"));throw e}const i=await le(t,e.project_id,e.item_id);if(!i)return ae(Z("Item",e.item_id));if("test-case"!==i.type)return ae(te(`Item '${e.item_id}' is a ${i.type}, not a test-case`));const a=(new Date).toISOString(),{error:s}=await t.from("test_results").insert({item_id:e.item_id,project_id:e.project_id,result:e.result,run_by:n||r,run_at:a,note:e.note??null});if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"test_result_recorded",old_data:null,new_data:{result:e.result,run_at:a,note:e.note},actor:"mcp_claude",session_id:ut()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ie(Y({item_id:e.item_id,result:e.result,run_at:a,run_by:n||r,note:e.note??null,message:`Test result '${e.result}' recorded for ${e.item_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(Ye,{recordTestResultHandler:()=>Ke});var Xe,Ze,et,tt,rt=j({"src/tools/record-test-result.ts"(){Q(),pe(),gt(),oe()}}),nt={};async function it(e){try{const t=Math.min(Math.max(e.depth??5,1),10),r=await J();if(!await le(r,e.project_id,e.item_id))return ae(Z("Item",e.item_id));const{data:n,error:i}=await r.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(i)throw new Error(i.message);const{data:a,error:s}=await r.from("items").select("id, title, type").eq("project_id",e.project_id).is("deleted_at",null);if(s)throw new Error(s.message);const o=new Map;for(const e of a??[])o.set(e.id,{title:e.title,type:e.type});const c=new Map,d=(e,t,r)=>{c.has(e)||c.set(e,[]),c.get(e).push({neighbor:t,relType:r})},l=e.relationship_types?new Set(e.relationship_types):null;for(const t of n??[]){const r=t.type;if(("upstream"===e.direction||"both"===e.direction)&&(Ze.includes(r)&&(l&&!l.has(r)||d(t.from_id,t.to_id,r)),et.includes(r))){const e=tt[r]??r;l&&!l.has(e)||d(t.to_id,t.from_id,e)}if(("downstream"===e.direction||"both"===e.direction)&&(et.includes(r)&&(l&&!l.has(r)||d(t.from_id,t.to_id,r)),Ze.includes(r))){const e=tt[r]??r;l&&!l.has(e)||d(t.to_id,t.from_id,e)}"related"===r&&(l&&!l.has("related")||(d(t.from_id,t.to_id,"related"),d(t.to_id,t.from_id,"related")))}const m=new Set;m.add(e.item_id);const p=[],u=[],h=c.get(e.item_id)??[];for(const e of h)u.push({id:e.neighbor,depth:1,relType:e.relType});for(;u.length>0&&p.length<Xe;){const{id:e,depth:r,relType:n}=u.shift();if(m.has(e)||r>t)continue;m.add(e);const i=o.get(e);if(i&&(p.push({id:e,title:i.title,type:i.type,relationship:n,depth:r}),r<t)){const t=c.get(e)??[];for(const e of t)m.has(e.neighbor)||u.push({id:e.neighbor,depth:r+1,relType:e.relType})}}return ie(Y({root:e.item_id,direction:e.direction,depth_limit:t,items:p,total_count:p.length,...p.length>=Xe?{truncated:!0,truncation_message:`Results capped at ${Xe} items. Use a smaller depth or filter by relationship_types.`}:{}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(nt,{traceHandler:()=>it});var at=j({"src/tools/trace.ts"(){Q(),pe(),oe(),Xe=200,Ze=["parent","verifies"],et=["child","verified_by"],tt={parent:"child",child:"parent",verifies:"verified_by",verified_by:"verifies",related:"related"}}}),st={};async function ot(e){try{const t=await J(),{data:r,error:n}=await t.from("projects").select("id, name").eq("id",e.project_id).is("deleted_at",null).maybeSingle();if(n)throw new Error(n.message);if(!r)return ae(Z("Project",e.project_id));const{data:i,error:a}=await t.from("items").select("id, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(a)throw new Error(a.message);const s=i??[],o={requirements:0,test_cases:0,notes:0},c=[],d=[];for(const e of s)if("requirement"===e.type){o.requirements++;const t=e.data?.tags?.domains??[];d.push({id:e.id,domains:t})}else"test-case"===e.type?(o.test_cases++,c.push(e.id)):"note"===e.type&&o.notes++;const l={passed:0,failed:0,blocked:0,not_run:0};if(c.length>0){const{data:r,error:n}=await t.from("test_results").select("item_id, result, run_at").eq("project_id",e.project_id).order("run_at",{ascending:!1});if(n)throw new Error(n.message);const i=new Map;for(const e of r??[])i.has(e.item_id)||i.set(e.item_id,e.result);for(const e of c){const t=i.get(e);t&&"not-run"!==t?"passed"===t?l.passed++:"failed"===t?l.failed++:"blocked"===t?l.blocked++:l.not_run++:l.not_run++}}const{data:m,error:p}=await t.from("item_relationships").select("from_id").eq("project_id",e.project_id).eq("type","verified_by");if(p)throw new Error(p.message);const u=new Set((m??[]).map(e=>e.from_id)),h=d.filter(e=>u.has(e.id)).length,_=d.length,g={covered:h,uncovered:_-h,total:_,percent:_>0?Math.round(h/_*1e3)/10:100},f=new Map;for(const e of d){const t=e.domains.length>0?e.domains:["(untagged)"],r=u.has(e.id);for(const e of t){f.has(e)||f.set(e,{covered:0,total:0});const t=f.get(e);t.total++,r&&t.covered++}}const y=Array.from(f.entries()).sort((e,t)=>e[0].localeCompare(t[0])).map(([e,t])=>({domain:e,covered:t.covered,total:t.total,percent:t.total>0?Math.round(t.covered/t.total*1e3)/10:100})),w=new Date;w.setDate(w.getDate()-7);const{count:b,error:v}=await t.from("change_history").select("id",{count:"exact",head:!0}).eq("project_id",e.project_id).gte("changed_at",w.toISOString());if(v)throw new Error(v.message);return ie(Y({project_id:e.project_id,project_name:r.name,counts:o,test_status:l,coverage:g,coverage_by_domain:y,recent_changes:b??0,last_updated:(new Date).toISOString()}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?ae(re()):ae(ne(e instanceof Error?e.message:String(e)))}}x(st,{projectSummaryHandler:()=>ot});var ct,dt,lt,mt=j({"src/tools/project-summary.ts"(){Q(),oe()}});function pt(e,t){return async r=>{let n;try{await J(),n=B()}catch{return t(r)}const i=function(e){const t=parseInt(process.env.ATOMS_RATE_LIMIT_RPM??"",10)||u,r=Date.now(),n=r-h;let i=_.get(e);if(i||(i={timestamps:[]},_.set(e,i)),i.timestamps=i.timestamps.filter(e=>e>n),i.timestamps.length>=t){const e=i.timestamps[0]+h-r;return{allowed:!1,retryAfterSeconds:Math.ceil(e/1e3)}}return i.timestamps.push(r),{allowed:!0}}(n);if(!i.allowed)return ae(X("Rate limit exceeded",[`Wait ${i.retryAfterSeconds} seconds before making more requests`,"Reduce the frequency of tool calls"]));const a=function(){const e=performance.now();return()=>Math.round(performance.now()-e)}();let s,o="success";try{const e=await t(r),n=e;if(!0===n?.isError){o="error";const e=n?.content;if(e?.[0]?.text)try{s=JSON.parse(e[0].text).message}catch{}}return e}catch(e){throw o="error",s=e instanceof Error?e.message:String(e),e}finally{const t={tool_name:e,params:r,status:o,duration_ms:a(),error_msg:s,project_id:r.project_id,session_id:dt,client_name:lt};try{!function(e,t,r){const n={...r.params};delete n.access_token,delete n.refresh_token,delete n.password,e.from("mcp_audit_log").insert({user_id:t,tool_name:r.tool_name,params:n,status:r.status,duration_ms:r.duration_ms,error_msg:r.error_msg??null,project_id:r.project_id??null,session_id:r.session_id??null,client_name:r.client_name??null}).then(({error:e})=>{e&&process.stderr.write(`[audit] Failed to log: ${e.message}\n`)})}(await J(),n,t)}catch{}}}}function ut(){return dt}var ht,_t,gt=j({"src/server.ts"(){S(),I(),Q(),oe(),de(),ct=new s.McpServer({name:"atoms-mcp-server",version:"0.1.0"}),dt=c.randomUUID(),lt=process.env.ATOMS_CLIENT_NAME??"unknown",ct.registerTool("atoms_list_projects",{title:"List ATOMS Projects",description:'List all projects the authenticated user has access to.\n\nThis is the ENTRY POINT tool. Call this first to discover project IDs,\nthen use those IDs with all other tools.\n\nArgs: None\n\nReturns:\n { status: "success", data: [{ id, name, description, org_id, created_at }] }\n\nExamples:\n - "What projects do I have?" → atoms_list_projects()\n - "Show my projects" → atoms_list_projects()\n\nWorkflow:\n atoms_list_projects → atoms_list_items(project_id) → atoms_get_item(project_id, item_id)',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_list_projects",async()=>{const{listProjectsHandler:e}=await Promise.resolve().then(()=>(_e(),ue));return e()})),ct.registerTool("atoms_list_items",{title:"List ATOMS Items",description:'List items in an ATOMS project with optional type/domain/level filters.\n\nThis is the primary ENTRY POINT for discovering items. Use it to browse\nproject contents before drilling into specific items with atoms_get_item.\n\nArgs:\n - project_id (string, UUID): The project to list items from\n - type (string, optional): Filter: requirement|test-case|note|table\n - domain (string, optional): Filter by domain tag\n - level (string, optional): Filter by level (System, Subsystem, Component)\n - limit (number, optional): Max results, 1-200 (default 50)\n - offset (number, optional): Pagination offset (default 0)\n\nReturns:\n { status: "success", data: [{ id, title, type, status, domains, level }], meta: { total_count, limit, offset, has_more } }\n\nExamples:\n - "Show me all requirements" → atoms_list_items(project_id, type="requirement")\n - "What test cases exist?" → atoms_list_items(project_id, type="test-case")\n\nErrors:\n - "Project not found" → verify project_id\n - "Access denied" → check org membership',inputSchema:{project_id:o.z.string().uuid("Must be a valid project UUID").describe("UUID of the project"),type:o.z.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),domain:o.z.string().optional().describe("Filter by domain tag (e.g., 'Safety', 'Performance')"),level:o.z.string().optional().describe("Filter by level (System, Subsystem, Component)"),limit:o.z.number().int().min(1).max(200).default(50).describe("Max results (default 50, max 200)"),offset:o.z.number().int().min(0).default(0).describe("Pagination offset (default 0)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_list_items",async e=>{const{listItemsHandler:t}=await Promise.resolve().then(()=>(ye(),ge));return t(e)})),ct.registerTool("atoms_get_item",{title:"Get ATOMS Item Details",description:'Get full details of a single item including relationships, test history, and ownership.\n\nUse after atoms_list_items or atoms_search to drill into a specific item.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID (e.g., "REQ-00001", "TC-00050")\n\nReturns:\n Full WorkItem with relationships, test runs, ownership, metadata.\n\nExamples:\n - "Show me requirement REQ-00001" → atoms_get_item(project_id, "REQ-00001")\n - "Get test case details" → atoms_get_item(project_id, "TC-00050")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID (e.g., REQ-00001)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_get_item",async e=>{const{getItemHandler:t}=await Promise.resolve().then(()=>(ve(),we));return t(e)})),ct.registerTool("atoms_search",{title:"Search ATOMS Items",description:'Full-text search across items in a project. Searches title, body, and summary.\n\nAlternative entry point to atoms_list_items when you know what you\'re looking for.\n\nArgs:\n - project_id (string, UUID): The project to search in\n - query (string): Search text (max 1000 chars)\n - type (string, optional): Filter by type\n - limit (number, optional): Max results, 1-100 (default 25)\n\nReturns:\n Matching items with @basic fields, ranked by relevance.\n\nExamples:\n - "Find braking requirements" → atoms_search(project_id, "braking system")\n - "Search for safety tests" → atoms_search(project_id, "safety", type="test-case")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),query:o.z.string().min(1).max(1e3).describe("Search text"),type:o.z.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),limit:o.z.number().int().min(1).max(100).default(25).describe("Max results (default 25, max 100)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_search",async e=>{const{searchHandler:t}=await Promise.resolve().then(()=>(Se(),je));return t(e)})),ce(ct,{appName:"coverage",htmlFile:"coverage-app.html",toolName:"atoms_get_coverage",title:"Get Test Coverage Report",description:'Find requirements without linked test cases (coverage gaps).\n\nEssential for compliance reporting in safety-critical industries.\n\nIn MCP App hosts (Claude, ChatGPT), renders a visual coverage heatmap.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to analyze\n - domain (string, optional): Filter by domain\n - level (string, optional): Filter by level\n\nReturns:\n { covered, uncovered, total, coverage_percent, uncovered_items: [...] }\n\nExamples:\n - "What\'s our test coverage?" → atoms_get_coverage(project_id)\n - "Safety coverage gaps?" → atoms_get_coverage(project_id, domain="Safety")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),domain:o.z.string().optional().describe("Filter by domain tag"),level:o.z.string().optional().describe("Filter by level")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:pt("atoms_get_coverage",async e=>{const{getCoverageHandler:t}=await Promise.resolve().then(()=>(Ee(),Ie));return t(e)})}),ct.registerTool("atoms_get_history",{title:"Get Item Change History",description:"Audit trail for an item — who changed what, when, and whether it was human or AI.\n\nShows actor attribution (user vs mcp_claude) and session grouping.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID\n - limit (number, optional): Max entries (default 20, max 100)\n\nReturns:\n Change entries with actor, session_id, event_type, changed_at, fields_changed.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID"),limit:o.z.number().int().min(1).max(100).default(20).describe("Max entries (default 20)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_get_history",async e=>{const{getHistoryHandler:t}=await Promise.resolve().then(()=>(He(),Te));return t(e)})),ce(ct,{appName:"mermaid",htmlFile:"mermaid-app.html",toolName:"atoms_export_mermaid",title:"Export Mermaid Diagram",description:"Generate a Mermaid diagram of the requirement/test hierarchy.\n\nOutput is paste-ready for markdown docs. Shows parent-child relationships\nand requirement-to-test-case verification links.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive pan/zoom diagram.\nFalls back to Mermaid text for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to visualize\n - root_item_id (string, optional): Start node (default: all roots)\n - depth (number, optional): Max depth (default 3, max 10)\n - include_tests (boolean, optional): Show test case links (default true)\n\nReturns:\n Mermaid graph string.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),root_item_id:o.z.string().optional().describe("Root item ID to start from"),depth:o.z.number().int().min(1).max(10).default(3).describe("Max depth"),include_tests:o.z.boolean().default(!0).describe("Include test case links")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},extraResourceDomains:["cdn.jsdelivr.net"],handler:pt("atoms_export_mermaid",async e=>{const{exportMermaidHandler:t}=await Promise.resolve().then(()=>(ze(),Ue));return t(e)})}),ct.registerTool("atoms_create_item",{title:"Create ATOMS Item",description:"Create a new requirement, test case, or note in a project.\n\nRequires editor or admin role. Changes are logged with AI actor attribution.\n\nArgs:\n - project_id (string, UUID): Target project\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_ids (string[], optional): Parent item IDs to link\n\nReturns:\n Created item with generated ID (e.g., \"REQ-00058\")\n\nSide effects:\n - Logs to change_history with actor='mcp_claude'\n - Creates relationships if parent_ids provided",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the target project"),type:o.z.enum(["requirement","test-case","note"]).describe("Item type"),title:o.z.string().min(1).max(500).describe("Item title"),body:o.z.string().optional().describe("Rich text body"),summary:o.z.string().optional().describe("Brief summary"),domains:o.z.array(o.z.string()).optional().describe("Domain tags"),level:o.z.string().optional().describe("System|Subsystem|Component"),parent_ids:o.z.array(o.z.string()).optional().describe("Parent item IDs to link")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},pt("atoms_create_item",async e=>{const{createItemHandler:t}=await Promise.resolve().then(()=>(Re(),De));return t(e)})),ct.registerTool("atoms_update_item",{title:"Update ATOMS Item",description:"Update an existing item's fields. Only provided fields are changed.\n\nRequires editor or admin role. Logs old/new data diff to change history.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to update\n - title, body, summary, domains, level, status: Fields to update (all optional)\n\nReturns:\n Updated item.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID to update"),title:o.z.string().min(1).max(500).optional().describe("New title"),body:o.z.string().optional().describe("New body"),summary:o.z.string().optional().describe("New summary"),domains:o.z.array(o.z.string()).optional().describe("New domain tags"),level:o.z.string().optional().describe("New level"),status:o.z.enum(["passed","failed","blocked","not-run"]).optional().describe("Test case status")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_update_item",async e=>{const{updateItemHandler:t}=await Promise.resolve().then(()=>(Ne(),Pe));return t(e)})),ct.registerTool("atoms_delete_item",{title:"Delete ATOMS Item",description:"Soft-delete an item (sets deleted_at, preserves for audit trail).\n\nRequires editor or admin role. The item is never truly removed.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to delete\n\nReturns:\n Confirmation with deleted item summary.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID to soft-delete")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1}},pt("atoms_delete_item",async e=>{const{deleteItemHandler:t}=await Promise.resolve().then(()=>(We(),$e));return t(e)})),ct.registerTool("atoms_link_items",{title:"Link ATOMS Items",description:"Add or remove relationships between items.\n\nSupports: parent, child, related, verifies, verified_by.\nUpdates both items' JSONB data and the shadow table.\n\nArgs:\n - project_id (string, UUID): Project containing both items\n - from_id (string): Source item\n - to_id (string): Target item\n - type (string): parent|child|related|verifies|verified_by\n - action (string): add|remove\n\nReturns:\n Updated relationship state for both items.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),from_id:o.z.string().min(1).max(20).describe("Source item ID"),to_id:o.z.string().min(1).max(20).describe("Target item ID"),type:o.z.enum(["parent","child","related","verifies","verified_by"]).describe("Relationship type"),action:o.z.enum(["add","remove"]).describe("Add or remove the relationship")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},pt("atoms_link_items",async e=>{const{linkItemsHandler:t}=await Promise.resolve().then(()=>(Be(),Le));return t(e)})),ce(ct,{appName:"import",htmlFile:"import-app.html",toolName:"atoms_bulk_import",title:"Bulk Import ATOMS Items",description:"Bulk create multiple items in a single tool call. Essential for AI agents\nthat generate 20+ requirements at once.\n\nChecks write access once, generates sequential IDs, batch inserts all items,\nand reports per-item errors without aborting the entire batch.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive results table.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Target project\n - items (array): Up to 100 items to create, each with:\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_id (string, optional): Parent item ID to link\n\nReturns:\n { created: number, items: [{ id, title, type }], errors: [] }\n\nLimits:\n - Maximum 100 items per call\n - If any item fails, others still succeed — errors reported separately\n\nSide effects:\n - Logs to change_history with actor='mcp_claude' for each created item\n - Creates parent relationships for items with parent_id",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the target project"),items:o.z.array(o.z.object({type:o.z.enum(["requirement","test-case","note"]).describe("Item type"),title:o.z.string().min(1).max(500).describe("Item title"),body:o.z.string().optional().describe("Rich text body"),summary:o.z.string().optional().describe("Brief summary"),domains:o.z.array(o.z.string()).optional().describe("Domain tags"),level:o.z.string().optional().describe("System|Subsystem|Component"),parent_id:o.z.string().optional().describe("Parent item ID to link")})).min(1).max(100).describe("Items to create (1-100)")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},handler:pt("atoms_bulk_import",async e=>{const{bulkImportHandler:t}=await Promise.resolve().then(()=>(Qe(),Ve));return t(e)})}),ct.registerTool("atoms_record_test_result",{title:"Record Test Result",description:"Record a pass/fail/blocked result for a test case.\n\nAppends to the test results history. Use atoms_get_item to see all results.\n\nArgs:\n - project_id (string, UUID): Project containing the test case\n - item_id (string): Test case ID (e.g., \"TC-00001\")\n - result (string): passed|failed|blocked|not-run\n - note (string, optional): Reason or comment for this result\n\nReturns:\n Recorded result with timestamp.\n\nSide effects:\n - Appends to test_results table\n - Logs to change_history with actor='mcp_claude'",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Test case ID (e.g., TC-00001)"),result:o.z.enum(["passed","failed","blocked","not-run"]).describe("Test result"),note:o.z.string().max(1e3).optional().describe("Reason or comment for this result")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},pt("atoms_record_test_result",async e=>{const{recordTestResultHandler:t}=await Promise.resolve().then(()=>(rt(),Ye));return t(e)})),ce(ct,{appName:"trace",htmlFile:"trace-app.html",toolName:"atoms_trace",title:"Trace Item Relationships",description:'Walk the traceability graph from a starting item.\n\nAnswers questions like "which requirements does TC-00003 verify?" or\n"if I change REQ-00001, what\'s affected downstream?"\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive graph visualization.\nFalls back to flat list for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Starting item ID\n - direction (string): upstream|downstream|both\n - upstream: follow parent/verifies links (dependencies)\n - downstream: follow child/verified_by links (dependents)\n - both: trace in both directions\n - depth (number, optional): Max traversal depth (default 5, max 10)\n - relationship_types (string[], optional): Filter to specific types (parent, child, related, verifies, verified_by)\n\nReturns:\n { root, direction, items: [{ id, title, type, relationship, depth }], total_count }\n\nExamples:\n - "What does TC-00003 verify?" → atoms_trace(project_id, "TC-00003", "upstream")\n - "What\'s affected by REQ-00001?" → atoms_trace(project_id, "REQ-00001", "downstream")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Starting item ID"),direction:o.z.enum(["upstream","downstream","both"]).describe("Traversal direction"),depth:o.z.number().int().min(1).max(10).default(5).describe("Max traversal depth (default 5)"),relationship_types:o.z.array(o.z.enum(["parent","child","related","verifies","verified_by"])).optional().describe("Filter to specific relationship types")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:pt("atoms_trace",async e=>{const{traceHandler:t}=await Promise.resolve().then(()=>(at(),nt));return t(e)})}),ce(ct,{appName:"summary",htmlFile:"summary-app.html",toolName:"atoms_project_summary",title:"Project Compliance Summary",description:'One-call project health and compliance dashboard.\n\nReturns item counts by type, test execution status, requirement coverage\n(overall and by domain), and recent change activity.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive dashboard with charts.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project to summarize\n\nReturns:\n { project_name, counts, test_status, coverage, coverage_by_domain, recent_changes, last_updated }\n\nExamples:\n - "Give me a compliance snapshot" → atoms_project_summary(project_id)\n - "How\'s our test coverage?" → atoms_project_summary(project_id)',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:pt("atoms_project_summary",async e=>{const{projectSummaryHandler:t}=await Promise.resolve().then(()=>(mt(),st));return t(e)})})}}),ft={};async function yt(){const e=c.randomBytes(32).toString("base64url"),t=c.createHash("sha256").update(e).digest("base64url"),r=`http://localhost:${ht}${_t}`,n=new a.URL(`${z}/auth/mcp-consent`);return n.searchParams.set("redirect_uri",r),n.searchParams.set("code_challenge",t),n.searchParams.set("code_challenge_method","S256"),new Promise((t,r)=>{let i;function s(){clearTimeout(i)}const o=d.createServer(async(n,i)=>{const c=new a.URL(n.url??"/",`http://localhost:${ht}`);if(c.pathname!==_t)return i.writeHead(404),void i.end("Not found");try{if("access_denied"===c.searchParams.get("error"))return i.writeHead(200,{"Content-Type":"text/html"}),i.end(bt("Authorization was cancelled.")),s(),o.close(),void r(new Error("Authorization cancelled by user."));const n=c.searchParams.get("code");if(n){const a=await fetch(`${O}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{"Content-Type":"application/json",apikey:q},body:JSON.stringify({auth_code:n,code_verifier:e})}),c=await a.json();if(!a.ok||!c.access_token){const e=c.error_description??c.msg??"Unknown error";return i.writeHead(200,{"Content-Type":"text/html"}),i.end(bt(`Code exchange failed: ${e}`)),s(),o.close(),void r(new Error(`Code exchange failed: ${e}`))}let d="authenticated";try{d=JSON.parse(Buffer.from(c.access_token.split(".")[1],"base64").toString()).email??d}catch{}const{createClient:l}=await import("@supabase/supabase-js"),m=l(O,q,{global:{headers:{Authorization:`Bearer ${c.access_token}`}}}),{data:p,error:u}=await m.from("org_members").select("org_id").limit(1);return u||!p||0===p.length?(i.writeHead(200,{"Content-Type":"text/html"}),i.end(bt('No ATOMS account found for this Google account.<br><br>Please sign up at <a href="https://x.atoms.tech">x.atoms.tech</a> first, then run this login command again.')),s(),o.close(),void r(new Error("No ATOMS account found. Sign up at x.atoms.tech first."))):(await T({access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Date.now()+1e3*(c.expires_in??3600),user_email:d}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(wt(d)),s(),o.close(),void t({email:d}))}const a=c.searchParams.get("access_token"),d=c.searchParams.get("refresh_token"),l=parseInt(c.searchParams.get("expires_in")??"3600",10);a&&d?(await T({access_token:a,refresh_token:d,expires_at:Date.now()+1e3*l}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(wt("authenticated")),s(),o.close(),t({email:"authenticated"})):(i.writeHead(200,{"Content-Type":"text/html"}),i.end("<!DOCTYPE html>\n<html>\n<head><title>ATOMS MCP Login</title></head>\n<body>\n <h1>Completing login...</h1>\n <script>\n const hash = window.location.hash.substring(1);\n const params = new URLSearchParams(hash);\n const data = {\n access_token: params.get('access_token'),\n refresh_token: params.get('refresh_token'),\n expires_in: params.get('expires_in'),\n user_email: ''\n };\n if (data.access_token) {\n // Decode JWT to get email\n try {\n const payload = JSON.parse(atob(data.access_token.split('.')[1]));\n data.user_email = payload.email || '';\n } catch(e) {}\n fetch('/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data)\n }).then(() => {\n document.body.innerHTML = '<h1>Login successful! You can close this tab.</h1>';\n }).catch(() => {\n document.body.innerHTML = '<h1>Login failed. Please try again.</h1>';\n });\n } else {\n document.body.innerHTML = '<h1>Login failed — no tokens received.</h1>';\n }\n <\/script>\n</body>\n</html>"))}catch(e){i.writeHead(500),i.end("Authentication failed"),s(),o.close(),r(e)}});o.on("request",async(e,n)=>{if("POST"===e.method&&"/token"===e.url){let i="";e.on("data",e=>{i+=e.toString()}),e.on("end",async()=>{try{const e=JSON.parse(i);await T({access_token:e.access_token,refresh_token:e.refresh_token,expires_at:Date.now()+1e3*(e.expires_in??3600),user_email:e.user_email}),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({ok:!0})),s(),o.close(),t({email:e.user_email??"authenticated"})}catch(e){n.writeHead(500),n.end("Failed to store credentials"),s(),o.close(),r(e)}})}}),o.listen(ht,"127.0.0.1",async()=>{process.stderr.write("\nOpening browser for ATOMS login...\n"),process.stderr.write(`If the browser doesn't open, visit:\n${n.toString()}\n\n`);try{const{default:e}=await import("open");await e(n.toString())}catch{process.stderr.write("Could not open browser automatically. Please open the URL above.\n")}}),i=setTimeout(()=>{o.close(),r(new Error("Login timed out after 60 seconds. Run 'npx @atoms-tech/atoms-mcp login' to try again."))},6e4)})}function wt(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Authorized</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #052e16;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 {\n font-size: 22px;\n font-weight: 600;\n margin-bottom: 8px;\n color: #22c55e;\n }\n .email {\n font-size: 14px;\n color: #a3a3a3;\n margin-bottom: 32px;\n }\n .permissions {\n text-align: left;\n background: #0a0a0a;\n border: 1px solid #262626;\n border-radius: 12px;\n padding: 20px;\n margin-bottom: 24px;\n }\n .permissions h3 {\n font-size: 12px;\n font-weight: 600;\n color: #737373;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 12px;\n }\n .perm-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 0;\n font-size: 14px;\n color: #d4d4d4;\n }\n .perm-item .dot {\n width: 6px;\n height: 6px;\n background: #22c55e;\n border-radius: 50%;\n flex-shrink: 0;\n }\n .closing {\n font-size: 13px;\n color: #525252;\n }\n .closing span { color: #a3a3a3; font-variant-numeric: tabular-nums; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>\n </svg>\n </div>\n <h1>Authorization Granted</h1>\n <p class="email">${e}</p>\n\n <div class="permissions">\n <h3>ATOMS MCP can now</h3>\n <div class="perm-item"><span class="dot"></span> Read your projects and requirements</div>\n <div class="perm-item"><span class="dot"></span> Create and edit requirements and test cases</div>\n <div class="perm-item"><span class="dot"></span> Record test results (pass/fail)</div>\n <div class="perm-item"><span class="dot"></span> Manage traceability relationships</div>\n <div class="perm-item"><span class="dot"></span> Export coverage reports and diagrams</div>\n </div>\n\n <p class="closing">This tab will close in <span id="countdown">5</span>s</p>\n </div>\n\n <script>\n let seconds = 5;\n const el = document.getElementById('countdown');\n const timer = setInterval(() => {\n seconds--;\n el.textContent = seconds;\n if (seconds <= 0) {\n clearInterval(timer);\n window.close();\n // If window.close() is blocked (not opened by script), update message\n setTimeout(() => {\n document.querySelector('.closing').textContent = 'You can close this tab now.';\n }, 500);\n }\n }, 1000);\n <\/script>\n</body>\n</html>`}function bt(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Failed</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #450a0a;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #ef4444; }\n .reason { font-size: 14px; color: #a3a3a3; line-height: 1.6; }\n .reason a { color: #7c3aed; text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class="reason">${e}</p>\n </div>\n</body>\n</html>`}x(ft,{login:()=>yt});var vt=j({"src/auth/login.ts"(){D(),M(),ht=19275,_t="/callback"}});gt();var jt=process.argv.slice(2)[0];(async function(){switch(jt){case"login":{const{login:e}=await Promise.resolve().then(()=>(vt(),ft));try{const{email:t}=await e();process.stderr.write(`\n ✓ Authenticated as ${t}\n`),process.stderr.write(" ✓ Credentials saved to ~/.atoms/credentials.json\n\n"),process.stderr.write(" Next steps:\n"),process.stderr.write(" $ claude mcp add atoms -- npx -y @atoms-tech/atoms-mcp\n"),process.stderr.write("\n Or add to Claude Desktop (Settings → MCP → Edit Config):\n"),process.stderr.write(' {\n "mcpServers": {\n "atoms": {\n "command": "npx",\n "args": ["-y", "@atoms-tech/atoms-mcp"]\n }\n }\n }\n\n')}catch(e){process.stderr.write(`\n ✗ ${e instanceof Error?e.message:String(e)}\n\n`),process.exit(1)}break}case"whoami":{const{getValidToken:e}=await Promise.resolve().then(()=>(L(),R));try{const{email:t,user_id:r}=await e();process.stderr.write(`\n ✓ Logged in as ${t}\n User ID: ${r}\n\n`)}catch(e){process.stderr.write("\n ✗ Not authenticated.\n Run: npx @atoms-tech/atoms-mcp login\n\n"),process.exit(1)}break}case"logout":{const{clearCredentials:e}=await Promise.resolve().then(()=>(D(),k));await e(),process.stderr.write("Logged out. Credentials removed.\n"),process.stderr.write("Run 'npx @atoms-tech/atoms-mcp login' to authenticate again.\n");break}case"--version":case"-v":process.stderr.write("@atoms-tech/atoms-mcp v0.1.0\n");break;case"--help":case"-h":process.stderr.write("ATOMS MCP Server — AI agent integration for requirements management\n\nUsage:\n npx @atoms-tech/atoms-mcp Start MCP server (stdio)\n npx @atoms-tech/atoms-mcp login Authenticate with ATOMS.tech\n npx @atoms-tech/atoms-mcp logout Switch account / clear credentials\n npx @atoms-tech/atoms-mcp whoami Show current user\n npx @atoms-tech/atoms-mcp --version Show version\n");break;default:{process.stderr.write("[atoms-mcp] Starting MCP server via stdio...\n");try{const{getValidToken:e}=await Promise.resolve().then(()=>(L(),R)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch{process.stderr.write("[atoms-mcp] No credentials found. Starting login flow...\n");try{const{login:e}=await Promise.resolve().then(()=>(vt(),ft)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch(e){process.stderr.write(`[atoms-mcp] Login failed: ${e instanceof Error?e.message:String(e)}\n[atoms-mcp] Starting server without auth. Tools will fail until you run: npx @atoms-tech/atoms-mcp login\n`)}}const e=new l.StdioServerTransport;await ct.connect(e),process.stderr.write("[atoms-mcp] MCP server running. Waiting for tool calls...\n")}}})().catch(e=>{process.stderr.write(`[atoms-mcp] Fatal: ${e instanceof Error?e.message:String(e)}\n`),process.exit(1)});
2
+ "use strict";var e=require("fs/promises"),t=require("os"),n=require("path"),r=require("@supabase/supabase-js"),i=require("@modelcontextprotocol/ext-apps/server"),a=require("url"),s=require("@modelcontextprotocol/sdk/server/mcp.js"),o=require("zod"),c=require("crypto"),d=require("http"),l=require("@modelcontextprotocol/sdk/server/stdio.js"),m="undefined"!=typeof document?document.currentScript:null;function p(e){return e&&e.__esModule?e:{default:e}}var u,h,_,g,f,y=p(e),w=p(n),b=Object.defineProperty,v=Object.getOwnPropertyNames,j=(e,t)=>function(){return e&&(t=(0,e[v(e)[0]])(e=0)),t},x=(e,t)=>{for(var n in t)b(e,n,{get:t[n],enumerable:!0})},S=j({"src/middleware/audit.ts"(){}}),k=j({"src/middleware/rate-limiter.ts"(){u=60,h=6e4,_=new Map}}),I={};async function E(){try{const t=await e.readFile(f,"utf-8"),n=JSON.parse(t);return n.access_token&&n.refresh_token?n:null}catch{return null}}async function T(t){await e.mkdir(g,{recursive:!0,mode:448}),await e.writeFile(f,JSON.stringify(t,null,2),{mode:384})}async function C(){const e=await E();return!!e&&e.expires_at>Date.now()+6e4}function H(e){return e.expires_at>Date.now()+6e4}async function O(){try{await e.unlink(f)}catch{}}function M(){return f}x(I,{clearCredentials:()=>O,getCredentialsPath:()=>M,hasValidCredentials:()=>C,isTokenValid:()=>H,readCredentials:()=>E,writeCredentials:()=>T});var R,q,U,D=j({"src/auth/token-store.ts"(){g=n.join(t.homedir(),".atoms"),f=n.join(g,"credentials.json")}}),z=j({"src/config.ts"(){R="https://gmebjyhomsbvhrxffzre.supabase.co",q="sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH",U=process.env.ATOMS_APP_URL??"https://x.atoms.tech"}}),P={};async function A(){const e=process.env.ATOMS_ACCESS_TOKEN;if(e){const t=N(e);return{access_token:e,refresh_token:"",user_id:t.sub,email:t.email??""}}const t=await E();if(!t)throw new Error("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' to connect your ATOMS account.");if(t.expires_at>Date.now()+6e4){const e=N(t.access_token);return{access_token:t.access_token,refresh_token:t.refresh_token,user_id:e.sub,email:e.email??t.user_email??""}}process.stderr.write("[atoms-mcp] Refreshing access token...\n");const n=r.createClient(R,q),{data:i,error:a}=await n.auth.refreshSession({refresh_token:t.refresh_token});if(a||!i.session)throw new Error(`Token refresh failed: ${a?.message??"No session returned"}. Re-run 'npx @atoms-tech/atoms-mcp login'.`);const s=i.session;return await T({access_token:s.access_token,refresh_token:s.refresh_token,expires_at:Date.now()+1e3*(s.expires_in??3600),user_email:s.user?.email}),{access_token:s.access_token,refresh_token:s.refresh_token,user_id:s.user?.id??"",email:s.user?.email??""}}function N(e){const t=e.split(".");if(3!==t.length)throw new Error("Invalid JWT format");const n=t[1].replace(/-/g,"+").replace(/_/g,"/").padEnd(t[1].length+(4-t[1].length%4)%4,"="),r=JSON.parse(Buffer.from(n,"base64").toString("utf-8"));if(!r.sub)throw new Error("JWT missing 'sub' claim");return r}x(P,{getValidToken:()=>A});var $,W,F,L,J=j({"src/auth/refresh.ts"(){D(),z()}});async function V(){if($&&L>Date.now()+6e4)return $;$&&process.stderr.write("[atoms-mcp] Token expiring, refreshing client...\n");const{access_token:e,user_id:t,email:n}=await A(),i=e.split(".");if(3===i.length)try{const e=JSON.parse(Buffer.from(i[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString());L=1e3*(e.exp??0)}catch{L=Date.now()+36e5}const a=r.createClient(R,q,{global:{headers:{Authorization:`Bearer ${e}`}}});return W=t,F=n,$=a}function B(){if(!W)throw new Error("Not authenticated. Call getClient() first.");return W}async function G(e,t){const n=B(),{data:r,error:i}=await e.from("projects").select("org_id").eq("id",t).maybeSingle();if(i||!r)throw new Error(`Project '${t}' not found`);const{data:a,error:s}=await e.from("org_members").select("role").eq("user_id",n).eq("org_id",r.org_id).maybeSingle();if(s||!a){const{data:t}=await e.rpc("is_platform_admin");if(!0===t)return"admin";throw new Error("Not a member of this project's organization")}if("viewer"===a.role)throw new Error("VIEWER_ROLE");return a.role}var Y,Q=j({"src/db/client.ts"(){J(),z(),$=null,W=null,F=null,L=0}});function K(e,t){return{status:"success",data:e,...t?{meta:t}:{}}}function X(e,t,n){return{total_count:e,limit:t,offset:n,has_more:n+t<e}}function Z(e,t){return{status:"error",message:e,next_steps:t}}function ee(e,t){return Z(`${e} '${t}' not found`,[`Verify the ${e.toLowerCase()} ID is correct`,"Use atoms_list_items or atoms_search to find valid IDs"])}function te(e){return Z(`${e} role cannot modify project data`,["Contact your org admin to upgrade your role to editor","Use read-only tools (atoms_list_items, atoms_get_item, atoms_search) instead"])}function ne(e){return Z(`Validation error: ${e}`,["Check the parameter types and constraints in the tool description","Ensure all required parameters are provided"])}function re(){return Z("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' first.",["Run: npx @atoms-tech/atoms-mcp login","Or set ATOMS_ACCESS_TOKEN environment variable"])}function ie(e){return Z(`Database error: ${e}`,["This may be a temporary issue — try again","If the error persists, check that the project_id is valid"])}function ae(e){const t=JSON.stringify(e,null,2);if(t.length>Y&&"success"===e.status&&Array.isArray(e.data)){const t=Math.max(1,Math.floor(e.data.length/2)),n={...e,data:e.data.slice(0,t),meta:{...e.meta,truncated:!0,truncation_message:`Response truncated from ${e.data.length} to ${t} items. Use 'offset' parameter or add filters to see more results.`}};return{content:[{type:"text",text:JSON.stringify(n,null,2)}],structuredContent:n}}return{content:[{type:"text",text:t}],structuredContent:e}}function se(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}],structuredContent:e,isError:!0}}var oe,ce=j({"src/tools/_base.ts"(){Y=25e3}});function de(e,t){const n=`ui://${t.toolName}/${t.htmlFile}`,r=function(e,t){const n=a.fileURLToPath("undefined"==typeof document?require("url").pathToFileURL(__filename).href:m&&"SCRIPT"===m.tagName.toUpperCase()&&m.src||new URL("index.cjs",document.baseURI).href),r=w.default.dirname(n);return w.default.resolve(r,"apps","src","apps",e,t)}(t.appName,t.htmlFile),s=[...oe.resourceDomains,...t.extraResourceDomains??[]];i.registerAppTool(e,t.toolName,{title:t.title,description:t.description,inputSchema:t.inputSchema,annotations:t.annotations,_meta:{ui:{resourceUri:n}}},t.handler),i.registerAppResource(e,`${t.title} UI`,n,{mimeType:i.RESOURCE_MIME_TYPE},async()=>{const e=await y.default.readFile(r,"utf-8");return{contents:[{uri:n,mimeType:i.RESOURCE_MIME_TYPE,text:e,_meta:{ui:{csp:{resourceDomains:s}}}}]}})}var le=j({"src/apps/register.ts"(){oe={resourceDomains:["fonts.googleapis.com","fonts.gstatic.com"]}}});async function me(e,t,n){const{data:r,error:i}=await e.from("items").select("*").eq("id",n).eq("project_id",t).is("deleted_at",null).maybeSingle();if(i)throw new Error(i.message);return r}async function pe(e,t,n){const r="requirement"===n?"REQ":"test-case"===n?"TC":"note"===n?"NOTE":"TABLE",{data:i,error:a}=await e.rpc("next_item_id",{p_project_id:t,p_prefix:r});if(a)throw new Error(`ID generation failed: ${a.message}`);return i}var ue=j({"src/db/queries.ts"(){}}),he={};async function _e(){try{const e=await V(),t=await async function(e){const{data:t,error:n}=await e.from("projects").select("id, name, description, org_id, created_at, organizations(name)").is("deleted_at",null).order("created_at",{ascending:!1});if(n)throw new Error(n.message);return(t??[]).map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.organizations?.name??"Unknown",created_at:e.created_at}))}(e);return ae(K(t.map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.org_name,created_at:e.created_at}))))}catch(e){return se(ie(e instanceof Error?e.message:"Unknown error"))}}x(he,{listProjectsHandler:()=>_e});var ge=j({"src/tools/list-projects.ts"(){Q(),ue(),ce()}}),fe={};async function ye(e){try{const t=await V(),{items:n,totalCount:r}=await async function(e,t,n){let r=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).order("created_at",{ascending:!0}).range(n.offset,n.offset+n.limit-1);n.type&&(r=r.eq("type",n.type));const{data:i,error:a,count:s}=await r;if(a)throw new Error(a.message);let o=i??[];return n.domain&&(o=o.filter(e=>(e.data?.tags?.domains??[]).includes(n.domain))),n.level&&(o=o.filter(e=>e.data?.tags?.level===n.level)),{items:o,totalCount:n.domain||n.level?o.length:s??0}}(t,e.project_id,{type:e.type,domain:e.domain,level:e.level,limit:e.limit,offset:e.offset});return ae(K(n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),X(r,e.limit,e.offset)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(fe,{listItemsHandler:()=>ye});var we=j({"src/tools/list-items.ts"(){Q(),ue(),ce()}}),be={};async function ve(e){try{const t=await V(),n=await me(t,e.project_id,e.item_id);if(!n)return se(ee("Item",e.item_id));const r=await async function(e,t,n){const{data:r,error:i}=await e.from("item_relationships").select("from_id, to_id, type").eq("project_id",t).or(`from_id.eq.${n},to_id.eq.${n}`);if(i)throw new Error(i.message);return r??[]}(t,e.project_id,e.item_id),i={parents:[],children:[],related:[],verified_by:[],verifies:[]};for(const t of r)if(t.from_id===e.item_id)t.type in i&&i[t.type].push(t.to_id);else{const e={parent:"children",child:"parents",verified_by:"verifies",verifies:"verified_by",related:"related"}[t.type]??t.type;e in i&&i[e].push(t.from_id)}let a=[];if("test-case"===n.type){const{data:n,error:r}=await t.from("test_results").select("result, run_by, run_at, note").eq("item_id",e.item_id).eq("project_id",e.project_id).order("run_at",{ascending:!1}).limit(20);!r&&n&&(a=n)}return ae(K({id:n.id,type:n.type,title:n.title,summary:n.data?.summary,body:n.data?.body,tags:n.data?.tags??{domains:[]},ownership:n.data?.ownership??{primary:null,additional:[]},relationships:i,links:n.data?.links??[],status:n.data?.status,..."test-case"===n.type?{latest_result:a[0]?.result??"not-run",test_results:a}:{},metadata:{created_at:n.created_at,created_by:n.created_by,updated_at:n.updated_at,updated_by:n.updated_by,source_id:n.data?.metadata?.source_id}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(be,{getItemHandler:()=>ve});var je=j({"src/tools/get-item.ts"(){Q(),ue(),ce()}}),xe={};async function Se(e){try{const t=await V(),{items:n,totalCount:r}=await async function(e,t,n,r){const i=n.trim();if(!i)return{items:[],totalCount:0};let a=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).or(`title.ilike.%${i}%,data->>body.ilike.%${i}%,data->>summary.ilike.%${i}%`).limit(r.limit);r.type&&(a=a.eq("type",r.type));const{data:s,error:o,count:c}=await a;if(o)throw new Error(o.message);return{items:s??[],totalCount:c??0}}(t,e.project_id,e.query,{type:e.type,limit:e.limit});return ae(K(n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),X(r,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(xe,{searchHandler:()=>Se});var ke=j({"src/tools/search.ts"(){Q(),ue(),ce()}}),Ie={};async function Ee(e){try{const t=await V(),{covered:n,uncovered:r,total:i}=await async function(e,t,n){const{data:r,error:i}=await e.from("items").select("*").eq("project_id",t).eq("type","requirement").is("deleted_at",null);if(i)throw new Error(i.message);const a=(r??[]).filter(e=>!(n.domain&&!(e.data?.tags?.domains??[]).includes(n.domain)||n.level&&e.data?.tags?.level!==n.level)),{data:s,error:o}=await e.from("item_relationships").select("from_id").eq("project_id",t).eq("type","verified_by");if(o)throw new Error(o.message);const c=new Set((s??[]).map(e=>e.from_id));return{covered:a.filter(e=>c.has(e.id)),uncovered:a.filter(e=>!c.has(e.id)),total:a.length}}(t,e.project_id,{domain:e.domain,level:e.level}),a=r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level}));return ae(K({covered:n.length,uncovered:r.length,total:i,coverage_percent:i>0?Math.round(n.length/i*1e3)/10:100,uncovered_items:a}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Ie,{getCoverageHandler:()=>Ee});var Te=j({"src/tools/get-coverage.ts"(){Q(),ue(),ce()}}),Ce={};async function He(e){try{const t=await V(),n=await async function(e,t,n,r){const{data:i,error:a}=await e.from("change_history").select("*").eq("item_id",n).eq("project_id",t).order("changed_at",{ascending:!1}).limit(r);if(a)throw new Error(a.message);return i??[]}(t,e.project_id,e.item_id,e.limit),r=n.map(e=>{const t=[],n=e.old_data,r=e.new_data;if(n&&r){const e=new Set([...Object.keys(n),...Object.keys(r)]);for(const i of e)JSON.stringify(n[i])!==JSON.stringify(r[i])&&t.push(i)}else r?t.push("(created)"):n&&t.push("(deleted)");return{id:e.id,event_type:e.event_type,changed_by:e.changed_by??null,changed_at:e.changed_at,actor:e.actor??"user",session_id:e.session_id??null,fields_changed:t}});return ae(K(r,X(r.length,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Ce,{getHistoryHandler:()=>He});var Oe=j({"src/tools/get-history.ts"(){Q(),ue(),ce()}}),Me={};async function Re(e){try{const t=await V(),{data:n,error:r}=await t.from("items").select("id, title, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(r)throw new Error(r.message);if(!n||0===n.length)return ae(K({mermaid:"graph TD\n empty[No items in project]"}));const{data:i,error:a}=await t.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(a)throw new Error(a.message);const s=new Map;for(const e of n)s.set(e.id,e);const o=["graph TD"],c=new Set,d=new Set,l=(new Set((i??[]).filter(e=>"child"===e.type).map(e=>e.to_id)),new Set((i??[]).filter(e=>"parent"===e.type).map(e=>e.from_id)));let m;m=e.root_item_id?[e.root_item_id]:n.filter(e=>!l.has(e.id)).filter(t=>!(!e.include_tests&&"test-case"===t.type)).map(e=>e.id);const p=m.map(e=>({id:e,depth:0}));for(;p.length>0;){const{id:t,depth:n}=p.shift();if(c.has(t)||n>e.depth)continue;c.add(t);const r=s.get(t);if(!r)continue;if(!e.include_tests&&"test-case"===r.type)continue;const a=qe(r.title),l="requirement"===r.type?`["${a}"]`:"test-case"===r.type?`(["${a}"])`:`("${a}")`;o.push(` ${t}${l}`);for(const r of i??[]){if(r.from_id!==t)continue;const i=s.get(r.to_id);if(!i)continue;if(!e.include_tests&&"test-case"===i.type)continue;const a=`${r.from_id}-${r.type}-${r.to_id}`;d.has(a)||(d.add(a),"child"===r.type?o.push(` ${t} --\x3e ${r.to_id}`):"verified_by"===r.type?o.push(` ${t} -.->|verified_by| ${r.to_id}`):"related"===r.type&&o.push(` ${t} -.- ${r.to_id}`),!c.has(r.to_id)&&n+1<=e.depth&&p.push({id:r.to_id,depth:n+1}))}}return ae(K({mermaid:o.join("\n"),node_count:c.size,edge_count:d.size}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}function qe(e){return e.replace(/"/g,"'").replace(/[[\]{}()]/g,"").substring(0,50)}x(Me,{exportMermaidHandler:()=>Re});var Ue=j({"src/tools/export-mermaid.ts"(){Q(),ce()}}),De={};async function ze(e){try{const t=await V(),n=B();try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const r=await pe(t,e.project_id,e.type),i={id:r,type:e.type,title:e.title,summary:e.summary,body:e.body,tags:{domains:e.domains??[],level:e.level},ownership:{primary:null,additional:[]},relationships:{parents:e.parent_ids??[],children:[],related:[]},links:[],metadata:{created_at:(new Date).toISOString(),created_by:n,updated_at:(new Date).toISOString(),updated_by:n}},{error:a}=await t.from("items").insert({id:r,project_id:e.project_id,type:e.type,title:e.title,data:i,created_by:n,updated_by:n}).select().single();if(a)throw new Error(a.message);if(e.parent_ids&&e.parent_ids.length>0){const n=e.parent_ids.map(t=>({from_id:r,to_id:t,type:"parent",project_id:e.project_id}));await t.from("item_relationships").insert(n).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: relationship sync failed: ${e.message}\n`)})}return await t.from("change_history").insert({item_id:r,project_id:e.project_id,changed_by:n,event_type:"created",old_data:null,new_data:i,actor:"mcp_claude",session_id:ht()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ae(K({id:r,type:e.type,title:e.title,project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(De,{createItemHandler:()=>ze});var Pe=j({"src/tools/create-item.ts"(){Q(),ue(),ft(),ce()}}),Ae={};async function Ne(e){try{const t=await V(),n=B();try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const r=await me(t,e.project_id,e.item_id);if(!r)return se(ee("Item",e.item_id));const i={...r.data},a={...r.data};void 0!==e.title&&(a.title=e.title),void 0!==e.body&&(a.body=e.body),void 0!==e.summary&&(a.summary=e.summary),void 0!==e.domains&&(a.tags={...a.tags,domains:e.domains}),void 0!==e.level&&(a.tags={...a.tags,level:e.level}),void 0!==e.status&&(a.status=e.status),a.metadata={...a.metadata,updated_at:(new Date).toISOString(),updated_by:n};const{error:s}=await t.from("items").update({title:e.title??r.title,data:a,updated_by:n}).eq("id",e.item_id).eq("project_id",e.project_id);if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"updated",old_data:i,new_data:a,actor:"mcp_claude",session_id:ht()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ae(K({id:e.item_id,title:a.title,type:r.type,updated_fields:Object.keys(e).filter(t=>"project_id"!==t&&"item_id"!==t&&void 0!==e[t])}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Ae,{updateItemHandler:()=>Ne});var $e=j({"src/tools/update-item.ts"(){Q(),ue(),ft(),ce()}}),We={};async function Fe(e){try{const t=await V(),n=B();try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const r=await me(t,e.project_id,e.item_id);if(!r)return se(ee("Item",e.item_id));const{error:i}=await t.from("items").update({deleted_at:(new Date).toISOString()}).eq("id",e.item_id).eq("project_id",e.project_id);if(i)throw new Error(i.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"deleted",old_data:r.data,new_data:null,actor:"mcp_claude",session_id:ht()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ae(K({id:e.item_id,title:r.title,type:r.type,deleted:!0,message:`Item ${e.item_id} soft-deleted. It can still be found in audit logs.`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(We,{deleteItemHandler:()=>Fe});var Le=j({"src/tools/delete-item.ts"(){Q(),ue(),ft(),ce()}}),Je={};async function Ve(e){try{const t=await V(),n=B();if(e.from_id===e.to_id)return se(ne("Cannot create a relationship between an item and itself"));try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const r=await me(t,e.project_id,e.from_id);if(!r)return se(ee("Item",e.from_id));if(!await me(t,e.project_id,e.to_id))return se(ee("Item",e.to_id));if("add"===e.action){const{data:i}=await t.from("item_relationships").select("from_id").eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id).maybeSingle();if(!i){const{error:n}=await t.from("item_relationships").insert({from_id:e.from_id,to_id:e.to_id,type:e.type,project_id:e.project_id});if(n)throw new Error(n.message)}const a={...r.data},s="verified_by"===e.type||"verifies"===e.type?e.type:e.type+"s"=="parents"||"parent"===e.type?"parents":"child"===e.type?"children":"related";if(a.relationships){const t=a.relationships[s]??[];t.includes(e.to_id)||(a.relationships[s]=[...t,e.to_id])}await t.from("items").update({data:a,updated_by:n}).eq("id",e.from_id).eq("project_id",e.project_id)}else{const{error:i}=await t.from("item_relationships").delete().eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id);if(i)throw new Error(i.message);const a={...r.data},s="parent"===e.type?"parents":"child"===e.type?"children":e.type;if(a.relationships){const t=a.relationships[s]??[];a.relationships[s]=t.filter(t=>t!==e.to_id)}await t.from("items").update({data:a,updated_by:n}).eq("id",e.from_id).eq("project_id",e.project_id)}return await t.from("change_history").insert({item_id:e.from_id,project_id:e.project_id,changed_by:n,event_type:"updated",old_data:null,new_data:{relationship_change:{action:e.action,type:e.type,from:e.from_id,to:e.to_id}},actor:"mcp_claude",session_id:ht()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ae(K({action:e.action,type:e.type,from_id:e.from_id,to_id:e.to_id,message:`Relationship ${"add"===e.action?"added":"removed"}: ${e.from_id} --[${e.type}]--\x3e ${e.to_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Je,{linkItemsHandler:()=>Ve});var Be=j({"src/tools/link-items.ts"(){Q(),ue(),ft(),ce()}}),Ge={};async function Ye(e){try{if(!e.items||0===e.items.length)return se(ne("items array must contain at least 1 item"));if(e.items.length>100)return se(ne(`Maximum 100 items per call (received ${e.items.length}). Split into multiple calls.`));const t=await V(),n=B();try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const r=(new Date).toISOString(),i=ht(),a=[],s=[],o=[];for(let n=0;n<e.items.length;n++){const r=e.items[n];try{const i=await pe(t,e.project_id,r.type);o.push({itemId:i,input:r,index:n})}catch(e){s.push({index:n,title:r.title,error:`ID generation failed: ${e instanceof Error?e.message:String(e)}`})}}if(0===o.length)return ae(K({created:0,items:[],errors:s}));const c=[],d=new Map;for(const{itemId:t,input:i}of o){const a={id:t,type:i.type,title:i.title,summary:i.summary,body:i.body,tags:{domains:i.domains??[],level:i.level},ownership:{primary:null,additional:[]},relationships:{parents:i.parent_id?[i.parent_id]:[],children:[],related:[]},links:[],metadata:{created_at:r,created_by:n,updated_at:r,updated_by:n}};d.set(t,a),c.push({id:t,project_id:e.project_id,type:i.type,title:i.title,data:a,created_by:n,updated_by:n})}const{error:l}=await t.from("items").insert(c);if(l)for(const{itemId:e,input:n,index:r}of o){const i=c.find(t=>t.id===e);if(!i)continue;const{error:o}=await t.from("items").insert(i);o?s.push({index:r,title:n.title,error:o.message}):a.push({id:e,title:n.title,type:n.type})}else for(const{itemId:e,input:t}of o)a.push({id:e,title:t.title,type:t.type});const m=o.filter(({input:e})=>e.parent_id).filter(({itemId:e})=>a.some(t=>t.id===e)).map(({itemId:t,input:n})=>({from_id:t,to_id:n.parent_id,type:"parent",project_id:e.project_id}));m.length>0&&await t.from("item_relationships").insert(m).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk relationship sync failed: ${e.message}\n`)});const p=a.map(t=>({item_id:t.id,project_id:e.project_id,changed_by:n,event_type:"created",old_data:null,new_data:d.get(t.id)??null,actor:"mcp_claude",session_id:i}));return p.length>0&&await t.from("change_history").insert(p).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk change_history log failed: ${e.message}\n`)}),ae(K({created:a.length,items:a,errors:s.length>0?s:[],project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Ge,{bulkImportHandler:()=>Ye});var Qe=j({"src/tools/bulk-import.ts"(){Q(),ue(),ft(),ce()}}),Ke={};async function Xe(e){try{const t=await V(),n=B(),r=F??"";try{await G(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return se(te("Viewer"));throw e}const i=await me(t,e.project_id,e.item_id);if(!i)return se(ee("Item",e.item_id));if("test-case"!==i.type)return se(ne(`Item '${e.item_id}' is a ${i.type}, not a test-case`));const a=(new Date).toISOString(),{error:s}=await t.from("test_results").insert({item_id:e.item_id,project_id:e.project_id,result:e.result,run_by:r||n,run_at:a,note:e.note??null});if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"test_result_recorded",old_data:null,new_data:{result:e.result,run_at:a,note:e.note},actor:"mcp_claude",session_id:ht()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),ae(K({item_id:e.item_id,result:e.result,run_at:a,run_by:r||n,note:e.note??null,message:`Test result '${e.result}' recorded for ${e.item_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(Ke,{recordTestResultHandler:()=>Xe});var Ze,et,tt,nt,rt=j({"src/tools/record-test-result.ts"(){Q(),ue(),ft(),ce()}}),it={};async function at(e){try{const t=Math.min(Math.max(e.depth??5,1),10),n=await V();if(!await me(n,e.project_id,e.item_id))return se(ee("Item",e.item_id));const{data:r,error:i}=await n.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(i)throw new Error(i.message);const{data:a,error:s}=await n.from("items").select("id, title, type").eq("project_id",e.project_id).is("deleted_at",null);if(s)throw new Error(s.message);const o=new Map;for(const e of a??[])o.set(e.id,{title:e.title,type:e.type});const c=new Map,d=(e,t,n)=>{c.has(e)||c.set(e,[]),c.get(e).push({neighbor:t,relType:n})},l=e.relationship_types?new Set(e.relationship_types):null;for(const t of r??[]){const n=t.type;if(("upstream"===e.direction||"both"===e.direction)&&(et.includes(n)&&(l&&!l.has(n)||d(t.from_id,t.to_id,n)),tt.includes(n))){const e=nt[n]??n;l&&!l.has(e)||d(t.to_id,t.from_id,e)}if(("downstream"===e.direction||"both"===e.direction)&&(tt.includes(n)&&(l&&!l.has(n)||d(t.from_id,t.to_id,n)),et.includes(n))){const e=nt[n]??n;l&&!l.has(e)||d(t.to_id,t.from_id,e)}"related"===n&&(l&&!l.has("related")||(d(t.from_id,t.to_id,"related"),d(t.to_id,t.from_id,"related")))}const m=new Set;m.add(e.item_id);const p=[],u=[],h=c.get(e.item_id)??[];for(const e of h)u.push({id:e.neighbor,depth:1,relType:e.relType});for(;u.length>0&&p.length<Ze;){const{id:e,depth:n,relType:r}=u.shift();if(m.has(e)||n>t)continue;m.add(e);const i=o.get(e);if(i&&(p.push({id:e,title:i.title,type:i.type,relationship:r,depth:n}),n<t)){const t=c.get(e)??[];for(const e of t)m.has(e.neighbor)||u.push({id:e.neighbor,depth:n+1,relType:e.relType})}}return ae(K({root:e.item_id,direction:e.direction,depth_limit:t,items:p,total_count:p.length,...p.length>=Ze?{truncated:!0,truncation_message:`Results capped at ${Ze} items. Use a smaller depth or filter by relationship_types.`}:{}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(it,{traceHandler:()=>at});var st=j({"src/tools/trace.ts"(){Q(),ue(),ce(),Ze=200,et=["parent","verifies"],tt=["child","verified_by"],nt={parent:"child",child:"parent",verifies:"verified_by",verified_by:"verifies",related:"related"}}}),ot={};async function ct(e){try{const t=await V(),{data:n,error:r}=await t.from("projects").select("id, name").eq("id",e.project_id).is("deleted_at",null).maybeSingle();if(r)throw new Error(r.message);if(!n)return se(ee("Project",e.project_id));const{data:i,error:a}=await t.from("items").select("id, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(a)throw new Error(a.message);const s=i??[],o={requirements:0,test_cases:0,notes:0},c=[],d=[];for(const e of s)if("requirement"===e.type){o.requirements++;const t=e.data?.tags?.domains??[];d.push({id:e.id,domains:t})}else"test-case"===e.type?(o.test_cases++,c.push(e.id)):"note"===e.type&&o.notes++;const l={passed:0,failed:0,blocked:0,not_run:0};if(c.length>0){const{data:n,error:r}=await t.from("test_results").select("item_id, result, run_at").eq("project_id",e.project_id).order("run_at",{ascending:!1});if(r)throw new Error(r.message);const i=new Map;for(const e of n??[])i.has(e.item_id)||i.set(e.item_id,e.result);for(const e of c){const t=i.get(e);t&&"not-run"!==t?"passed"===t?l.passed++:"failed"===t?l.failed++:"blocked"===t?l.blocked++:l.not_run++:l.not_run++}}const{data:m,error:p}=await t.from("item_relationships").select("from_id").eq("project_id",e.project_id).eq("type","verified_by");if(p)throw new Error(p.message);const u=new Set((m??[]).map(e=>e.from_id)),h=d.filter(e=>u.has(e.id)).length,_=d.length,g={covered:h,uncovered:_-h,total:_,percent:_>0?Math.round(h/_*1e3)/10:100},f=new Map;for(const e of d){const t=e.domains.length>0?e.domains:["(untagged)"],n=u.has(e.id);for(const e of t){f.has(e)||f.set(e,{covered:0,total:0});const t=f.get(e);t.total++,n&&t.covered++}}const y=Array.from(f.entries()).sort((e,t)=>e[0].localeCompare(t[0])).map(([e,t])=>({domain:e,covered:t.covered,total:t.total,percent:t.total>0?Math.round(t.covered/t.total*1e3)/10:100})),w=new Date;w.setDate(w.getDate()-7);const{count:b,error:v}=await t.from("change_history").select("id",{count:"exact",head:!0}).eq("project_id",e.project_id).gte("changed_at",w.toISOString());if(v)throw new Error(v.message);return ae(K({project_id:e.project_id,project_name:n.name,counts:o,test_status:l,coverage:g,coverage_by_domain:y,recent_changes:b??0,last_updated:(new Date).toISOString()}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?se(re()):se(ie(e instanceof Error?e.message:String(e)))}}x(ot,{projectSummaryHandler:()=>ct});var dt,lt,mt,pt=j({"src/tools/project-summary.ts"(){Q(),ce()}});function ut(e,t){return async n=>{let r;try{await V(),r=B()}catch{return t(n)}const i=function(e){const t=parseInt(process.env.ATOMS_RATE_LIMIT_RPM??"",10)||u,n=Date.now(),r=n-h;let i=_.get(e);if(i||(i={timestamps:[]},_.set(e,i)),i.timestamps=i.timestamps.filter(e=>e>r),i.timestamps.length>=t){const e=i.timestamps[0]+h-n;return{allowed:!1,retryAfterSeconds:Math.ceil(e/1e3)}}return i.timestamps.push(n),{allowed:!0}}(r);if(!i.allowed)return se(Z("Rate limit exceeded",[`Wait ${i.retryAfterSeconds} seconds before making more requests`,"Reduce the frequency of tool calls"]));const a=function(){const e=performance.now();return()=>Math.round(performance.now()-e)}();let s,o="success";try{const e=await t(n),r=e;if(!0===r?.isError){o="error";const e=r?.content;if(e?.[0]?.text)try{s=JSON.parse(e[0].text).message}catch{}}return e}catch(e){throw o="error",s=e instanceof Error?e.message:String(e),e}finally{const t={tool_name:e,params:n,status:o,duration_ms:a(),error_msg:s,project_id:n.project_id,session_id:lt,client_name:mt};try{!function(e,t,n){const r={...n.params};delete r.access_token,delete r.refresh_token,delete r.password,e.from("mcp_audit_log").insert({user_id:t,tool_name:n.tool_name,params:r,status:n.status,duration_ms:n.duration_ms,error_msg:n.error_msg??null,project_id:n.project_id??null,session_id:n.session_id??null,client_name:n.client_name??null}).then(({error:e})=>{e&&process.stderr.write(`[audit] Failed to log: ${e.message}\n`)})}(await V(),r,t)}catch{}}}}function ht(){return lt}var _t,gt,ft=j({"src/server.ts"(){S(),k(),Q(),ce(),le(),dt=new s.McpServer({name:"atoms-mcp-server",version:"0.1.0"}),lt=c.randomUUID(),mt=process.env.ATOMS_CLIENT_NAME??"unknown",dt.registerTool("atoms_status",{title:"ATOMS MCP Health Check",description:'Check the health and authentication status of the ATOMS MCP server.\n\nCall this FIRST if other atoms tools fail or return auth errors.\nThis tool works WITHOUT authentication — it checks whether credentials\nexist and are valid, and tells the user exactly what to do if not.\n\nArgs: None\n\nReturns:\n { status: "authenticated"|"not_authenticated"|"expired", email, message, next_steps }\n\nExamples:\n - "Is ATOMS MCP working?" → atoms_status()\n - "Why are atoms tools failing?" → atoms_status()',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},async()=>{try{const{readCredentials:e,isTokenValid:t}=await Promise.resolve().then(()=>(D(),I)),n=await e();if(!n)return{content:[{type:"text",text:JSON.stringify({status:"not_authenticated",message:"No ATOMS credentials found. You need to login first.",next_steps:["1. Open a terminal on your machine","2. Run: npx @atoms-tech/atoms-mcp login","3. Complete the login in your browser","4. Restart Claude Desktop (quit and reopen)","5. Try your query again"]},null,2)}]};if(!t(n))try{const{getValidToken:e}=await Promise.resolve().then(()=>(J(),P)),{email:t}=await e();return{content:[{type:"text",text:JSON.stringify({status:"authenticated",email:t,message:"Token was expired but has been refreshed. You're good to go!"},null,2)}]}}catch{return{content:[{type:"text",text:JSON.stringify({status:"expired",email:n.user_email??"unknown",message:"Your session has expired and could not be refreshed.",next_steps:["1. Open a terminal on your machine","2. Run: npx @atoms-tech/atoms-mcp logout","3. Run: npx @atoms-tech/atoms-mcp login","4. Complete the login in your browser","5. Restart Claude Desktop (quit and reopen)"]},null,2)}]}}return{content:[{type:"text",text:JSON.stringify({status:"authenticated",email:n.user_email??"unknown",message:"ATOMS MCP is connected and authenticated. All tools are ready."},null,2)}]}}catch(e){return{content:[{type:"text",text:JSON.stringify({status:"error",message:`Health check failed: ${e instanceof Error?e.message:String(e)}`,next_steps:["1. Open a terminal","2. Run: npx @atoms-tech/atoms-mcp login","3. Restart Claude Desktop"]},null,2)}]}}}),dt.registerTool("atoms_list_projects",{title:"List ATOMS Projects",description:'List all projects the authenticated user has access to.\n\nThis is the ENTRY POINT tool. Call this first to discover project IDs,\nthen use those IDs with all other tools.\n\nArgs: None\n\nReturns:\n { status: "success", data: [{ id, name, description, org_id, created_at }] }\n\nExamples:\n - "What projects do I have?" → atoms_list_projects()\n - "Show my projects" → atoms_list_projects()\n\nWorkflow:\n atoms_list_projects → atoms_list_items(project_id) → atoms_get_item(project_id, item_id)',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_list_projects",async()=>{const{listProjectsHandler:e}=await Promise.resolve().then(()=>(ge(),he));return e()})),dt.registerTool("atoms_list_items",{title:"List ATOMS Items",description:'List items in an ATOMS project with optional type/domain/level filters.\n\nThis is the primary ENTRY POINT for discovering items. Use it to browse\nproject contents before drilling into specific items with atoms_get_item.\n\nArgs:\n - project_id (string, UUID): The project to list items from\n - type (string, optional): Filter: requirement|test-case|note|table\n - domain (string, optional): Filter by domain tag\n - level (string, optional): Filter by level (System, Subsystem, Component)\n - limit (number, optional): Max results, 1-200 (default 50)\n - offset (number, optional): Pagination offset (default 0)\n\nReturns:\n { status: "success", data: [{ id, title, type, status, domains, level }], meta: { total_count, limit, offset, has_more } }\n\nExamples:\n - "Show me all requirements" → atoms_list_items(project_id, type="requirement")\n - "What test cases exist?" → atoms_list_items(project_id, type="test-case")\n\nErrors:\n - "Project not found" → verify project_id\n - "Access denied" → check org membership',inputSchema:{project_id:o.z.string().uuid("Must be a valid project UUID").describe("UUID of the project"),type:o.z.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),domain:o.z.string().optional().describe("Filter by domain tag (e.g., 'Safety', 'Performance')"),level:o.z.string().optional().describe("Filter by level (System, Subsystem, Component)"),limit:o.z.number().int().min(1).max(200).default(50).describe("Max results (default 50, max 200)"),offset:o.z.number().int().min(0).default(0).describe("Pagination offset (default 0)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_list_items",async e=>{const{listItemsHandler:t}=await Promise.resolve().then(()=>(we(),fe));return t(e)})),dt.registerTool("atoms_get_item",{title:"Get ATOMS Item Details",description:'Get full details of a single item including relationships, test history, and ownership.\n\nUse after atoms_list_items or atoms_search to drill into a specific item.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID (e.g., "REQ-00001", "TC-00050")\n\nReturns:\n Full WorkItem with relationships, test runs, ownership, metadata.\n\nExamples:\n - "Show me requirement REQ-00001" → atoms_get_item(project_id, "REQ-00001")\n - "Get test case details" → atoms_get_item(project_id, "TC-00050")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID (e.g., REQ-00001)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_get_item",async e=>{const{getItemHandler:t}=await Promise.resolve().then(()=>(je(),be));return t(e)})),dt.registerTool("atoms_search",{title:"Search ATOMS Items",description:'Full-text search across items in a project. Searches title, body, and summary.\n\nAlternative entry point to atoms_list_items when you know what you\'re looking for.\n\nArgs:\n - project_id (string, UUID): The project to search in\n - query (string): Search text (max 1000 chars)\n - type (string, optional): Filter by type\n - limit (number, optional): Max results, 1-100 (default 25)\n\nReturns:\n Matching items with @basic fields, ranked by relevance.\n\nExamples:\n - "Find braking requirements" → atoms_search(project_id, "braking system")\n - "Search for safety tests" → atoms_search(project_id, "safety", type="test-case")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),query:o.z.string().min(1).max(1e3).describe("Search text"),type:o.z.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),limit:o.z.number().int().min(1).max(100).default(25).describe("Max results (default 25, max 100)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_search",async e=>{const{searchHandler:t}=await Promise.resolve().then(()=>(ke(),xe));return t(e)})),de(dt,{appName:"coverage",htmlFile:"coverage-app.html",toolName:"atoms_get_coverage",title:"Get Test Coverage Report",description:'Find requirements without linked test cases (coverage gaps).\n\nEssential for compliance reporting in safety-critical industries.\n\nIn MCP App hosts (Claude, ChatGPT), renders a visual coverage heatmap.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to analyze\n - domain (string, optional): Filter by domain\n - level (string, optional): Filter by level\n\nReturns:\n { covered, uncovered, total, coverage_percent, uncovered_items: [...] }\n\nExamples:\n - "What\'s our test coverage?" → atoms_get_coverage(project_id)\n - "Safety coverage gaps?" → atoms_get_coverage(project_id, domain="Safety")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),domain:o.z.string().optional().describe("Filter by domain tag"),level:o.z.string().optional().describe("Filter by level")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:ut("atoms_get_coverage",async e=>{const{getCoverageHandler:t}=await Promise.resolve().then(()=>(Te(),Ie));return t(e)})}),dt.registerTool("atoms_get_history",{title:"Get Item Change History",description:"Audit trail for an item — who changed what, when, and whether it was human or AI.\n\nShows actor attribution (user vs mcp_claude) and session grouping.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID\n - limit (number, optional): Max entries (default 20, max 100)\n\nReturns:\n Change entries with actor, session_id, event_type, changed_at, fields_changed.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID"),limit:o.z.number().int().min(1).max(100).default(20).describe("Max entries (default 20)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_get_history",async e=>{const{getHistoryHandler:t}=await Promise.resolve().then(()=>(Oe(),Ce));return t(e)})),de(dt,{appName:"mermaid",htmlFile:"mermaid-app.html",toolName:"atoms_export_mermaid",title:"Export Mermaid Diagram",description:"Generate a Mermaid diagram of the requirement/test hierarchy.\n\nOutput is paste-ready for markdown docs. Shows parent-child relationships\nand requirement-to-test-case verification links.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive pan/zoom diagram.\nFalls back to Mermaid text for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to visualize\n - root_item_id (string, optional): Start node (default: all roots)\n - depth (number, optional): Max depth (default 3, max 10)\n - include_tests (boolean, optional): Show test case links (default true)\n\nReturns:\n Mermaid graph string.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),root_item_id:o.z.string().optional().describe("Root item ID to start from"),depth:o.z.number().int().min(1).max(10).default(3).describe("Max depth"),include_tests:o.z.boolean().default(!0).describe("Include test case links")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},extraResourceDomains:["cdn.jsdelivr.net"],handler:ut("atoms_export_mermaid",async e=>{const{exportMermaidHandler:t}=await Promise.resolve().then(()=>(Ue(),Me));return t(e)})}),dt.registerTool("atoms_create_item",{title:"Create ATOMS Item",description:"Create a new requirement, test case, or note in a project.\n\nRequires editor or admin role. Changes are logged with AI actor attribution.\n\nArgs:\n - project_id (string, UUID): Target project\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_ids (string[], optional): Parent item IDs to link\n\nReturns:\n Created item with generated ID (e.g., \"REQ-00058\")\n\nSide effects:\n - Logs to change_history with actor='mcp_claude'\n - Creates relationships if parent_ids provided",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the target project"),type:o.z.enum(["requirement","test-case","note"]).describe("Item type"),title:o.z.string().min(1).max(500).describe("Item title"),body:o.z.string().optional().describe("Rich text body"),summary:o.z.string().optional().describe("Brief summary"),domains:o.z.array(o.z.string()).optional().describe("Domain tags"),level:o.z.string().optional().describe("System|Subsystem|Component"),parent_ids:o.z.array(o.z.string()).optional().describe("Parent item IDs to link")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},ut("atoms_create_item",async e=>{const{createItemHandler:t}=await Promise.resolve().then(()=>(Pe(),De));return t(e)})),dt.registerTool("atoms_update_item",{title:"Update ATOMS Item",description:"Update an existing item's fields. Only provided fields are changed.\n\nRequires editor or admin role. Logs old/new data diff to change history.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to update\n - title, body, summary, domains, level, status: Fields to update (all optional)\n\nReturns:\n Updated item.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID to update"),title:o.z.string().min(1).max(500).optional().describe("New title"),body:o.z.string().optional().describe("New body"),summary:o.z.string().optional().describe("New summary"),domains:o.z.array(o.z.string()).optional().describe("New domain tags"),level:o.z.string().optional().describe("New level"),status:o.z.enum(["passed","failed","blocked","not-run"]).optional().describe("Test case status")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_update_item",async e=>{const{updateItemHandler:t}=await Promise.resolve().then(()=>($e(),Ae));return t(e)})),dt.registerTool("atoms_delete_item",{title:"Delete ATOMS Item",description:"Soft-delete an item (sets deleted_at, preserves for audit trail).\n\nRequires editor or admin role. The item is never truly removed.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to delete\n\nReturns:\n Confirmation with deleted item summary.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Item ID to soft-delete")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1}},ut("atoms_delete_item",async e=>{const{deleteItemHandler:t}=await Promise.resolve().then(()=>(Le(),We));return t(e)})),dt.registerTool("atoms_link_items",{title:"Link ATOMS Items",description:"Add or remove relationships between items.\n\nSupports: parent, child, related, verifies, verified_by.\nUpdates both items' JSONB data and the shadow table.\n\nArgs:\n - project_id (string, UUID): Project containing both items\n - from_id (string): Source item\n - to_id (string): Target item\n - type (string): parent|child|related|verifies|verified_by\n - action (string): add|remove\n\nReturns:\n Updated relationship state for both items.",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),from_id:o.z.string().min(1).max(20).describe("Source item ID"),to_id:o.z.string().min(1).max(20).describe("Target item ID"),type:o.z.enum(["parent","child","related","verifies","verified_by"]).describe("Relationship type"),action:o.z.enum(["add","remove"]).describe("Add or remove the relationship")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},ut("atoms_link_items",async e=>{const{linkItemsHandler:t}=await Promise.resolve().then(()=>(Be(),Je));return t(e)})),de(dt,{appName:"import",htmlFile:"import-app.html",toolName:"atoms_bulk_import",title:"Bulk Import ATOMS Items",description:"Bulk create multiple items in a single tool call. Essential for AI agents\nthat generate 20+ requirements at once.\n\nChecks write access once, generates sequential IDs, batch inserts all items,\nand reports per-item errors without aborting the entire batch.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive results table.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Target project\n - items (array): Up to 100 items to create, each with:\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_id (string, optional): Parent item ID to link\n\nReturns:\n { created: number, items: [{ id, title, type }], errors: [] }\n\nLimits:\n - Maximum 100 items per call\n - If any item fails, others still succeed — errors reported separately\n\nSide effects:\n - Logs to change_history with actor='mcp_claude' for each created item\n - Creates parent relationships for items with parent_id",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the target project"),items:o.z.array(o.z.object({type:o.z.enum(["requirement","test-case","note"]).describe("Item type"),title:o.z.string().min(1).max(500).describe("Item title"),body:o.z.string().optional().describe("Rich text body"),summary:o.z.string().optional().describe("Brief summary"),domains:o.z.array(o.z.string()).optional().describe("Domain tags"),level:o.z.string().optional().describe("System|Subsystem|Component"),parent_id:o.z.string().optional().describe("Parent item ID to link")})).min(1).max(100).describe("Items to create (1-100)")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},handler:ut("atoms_bulk_import",async e=>{const{bulkImportHandler:t}=await Promise.resolve().then(()=>(Qe(),Ge));return t(e)})}),dt.registerTool("atoms_record_test_result",{title:"Record Test Result",description:"Record a pass/fail/blocked result for a test case.\n\nAppends to the test results history. Use atoms_get_item to see all results.\n\nArgs:\n - project_id (string, UUID): Project containing the test case\n - item_id (string): Test case ID (e.g., \"TC-00001\")\n - result (string): passed|failed|blocked|not-run\n - note (string, optional): Reason or comment for this result\n\nReturns:\n Recorded result with timestamp.\n\nSide effects:\n - Appends to test_results table\n - Logs to change_history with actor='mcp_claude'",inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Test case ID (e.g., TC-00001)"),result:o.z.enum(["passed","failed","blocked","not-run"]).describe("Test result"),note:o.z.string().max(1e3).optional().describe("Reason or comment for this result")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},ut("atoms_record_test_result",async e=>{const{recordTestResultHandler:t}=await Promise.resolve().then(()=>(rt(),Ke));return t(e)})),de(dt,{appName:"trace",htmlFile:"trace-app.html",toolName:"atoms_trace",title:"Trace Item Relationships",description:'Walk the traceability graph from a starting item.\n\nAnswers questions like "which requirements does TC-00003 verify?" or\n"if I change REQ-00001, what\'s affected downstream?"\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive graph visualization.\nFalls back to flat list for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Starting item ID\n - direction (string): upstream|downstream|both\n - upstream: follow parent/verifies links (dependencies)\n - downstream: follow child/verified_by links (dependents)\n - both: trace in both directions\n - depth (number, optional): Max traversal depth (default 5, max 10)\n - relationship_types (string[], optional): Filter to specific types (parent, child, related, verifies, verified_by)\n\nReturns:\n { root, direction, items: [{ id, title, type, relationship, depth }], total_count }\n\nExamples:\n - "What does TC-00003 verify?" → atoms_trace(project_id, "TC-00003", "upstream")\n - "What\'s affected by REQ-00001?" → atoms_trace(project_id, "REQ-00001", "downstream")',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project"),item_id:o.z.string().min(1).max(20).describe("Starting item ID"),direction:o.z.enum(["upstream","downstream","both"]).describe("Traversal direction"),depth:o.z.number().int().min(1).max(10).default(5).describe("Max traversal depth (default 5)"),relationship_types:o.z.array(o.z.enum(["parent","child","related","verifies","verified_by"])).optional().describe("Filter to specific relationship types")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:ut("atoms_trace",async e=>{const{traceHandler:t}=await Promise.resolve().then(()=>(st(),it));return t(e)})}),de(dt,{appName:"summary",htmlFile:"summary-app.html",toolName:"atoms_project_summary",title:"Project Compliance Summary",description:'One-call project health and compliance dashboard.\n\nReturns item counts by type, test execution status, requirement coverage\n(overall and by domain), and recent change activity.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive dashboard with charts.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project to summarize\n\nReturns:\n { project_name, counts, test_status, coverage, coverage_by_domain, recent_changes, last_updated }\n\nExamples:\n - "Give me a compliance snapshot" → atoms_project_summary(project_id)\n - "How\'s our test coverage?" → atoms_project_summary(project_id)',inputSchema:{project_id:o.z.string().uuid().describe("UUID of the project")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:ut("atoms_project_summary",async e=>{const{projectSummaryHandler:t}=await Promise.resolve().then(()=>(pt(),ot));return t(e)})})}}),yt={};async function wt(){const e=c.randomBytes(32).toString("base64url"),t=c.createHash("sha256").update(e).digest("base64url"),n=`http://localhost:${_t}${gt}`,r=new a.URL(`${U}/auth/mcp-consent`);return r.searchParams.set("redirect_uri",n),r.searchParams.set("code_challenge",t),r.searchParams.set("code_challenge_method","S256"),new Promise((t,n)=>{let i;function s(){clearTimeout(i)}const o=d.createServer(async(r,i)=>{const c=new a.URL(r.url??"/",`http://localhost:${_t}`);if(c.pathname!==gt)return i.writeHead(404),void i.end("Not found");try{if("access_denied"===c.searchParams.get("error"))return i.writeHead(200,{"Content-Type":"text/html"}),i.end(vt("Authorization was cancelled.")),s(),o.close(),void n(new Error("Authorization cancelled by user."));const r=c.searchParams.get("code");if(r){const a=await fetch(`${R}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{"Content-Type":"application/json",apikey:q},body:JSON.stringify({auth_code:r,code_verifier:e})}),c=await a.json();if(!a.ok||!c.access_token){const e=c.error_description??c.msg??"Unknown error";return i.writeHead(200,{"Content-Type":"text/html"}),i.end(vt(`Code exchange failed: ${e}`)),s(),o.close(),void n(new Error(`Code exchange failed: ${e}`))}let d="authenticated";try{d=JSON.parse(Buffer.from(c.access_token.split(".")[1],"base64").toString()).email??d}catch{}const{createClient:l}=await import("@supabase/supabase-js"),m=l(R,q,{global:{headers:{Authorization:`Bearer ${c.access_token}`}}}),{data:p,error:u}=await m.from("org_members").select("org_id").limit(1);return u||!p||0===p.length?(i.writeHead(200,{"Content-Type":"text/html"}),i.end(vt('No ATOMS account found for this Google account.<br><br>Please sign up at <a href="https://x.atoms.tech">x.atoms.tech</a> first, then run this login command again.')),s(),o.close(),void n(new Error("No ATOMS account found. Sign up at x.atoms.tech first."))):(await T({access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Date.now()+1e3*(c.expires_in??3600),user_email:d}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(bt(d)),s(),o.close(),void t({email:d}))}const a=c.searchParams.get("access_token"),d=c.searchParams.get("refresh_token"),l=parseInt(c.searchParams.get("expires_in")??"3600",10);a&&d?(await T({access_token:a,refresh_token:d,expires_at:Date.now()+1e3*l}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(bt("authenticated")),s(),o.close(),t({email:"authenticated"})):(i.writeHead(200,{"Content-Type":"text/html"}),i.end("<!DOCTYPE html>\n<html>\n<head><title>ATOMS MCP Login</title></head>\n<body>\n <h1>Completing login...</h1>\n <script>\n const hash = window.location.hash.substring(1);\n const params = new URLSearchParams(hash);\n const data = {\n access_token: params.get('access_token'),\n refresh_token: params.get('refresh_token'),\n expires_in: params.get('expires_in'),\n user_email: ''\n };\n if (data.access_token) {\n // Decode JWT to get email\n try {\n const payload = JSON.parse(atob(data.access_token.split('.')[1]));\n data.user_email = payload.email || '';\n } catch(e) {}\n fetch('/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data)\n }).then(() => {\n document.body.innerHTML = '<h1>Login successful! You can close this tab.</h1>';\n }).catch(() => {\n document.body.innerHTML = '<h1>Login failed. Please try again.</h1>';\n });\n } else {\n document.body.innerHTML = '<h1>Login failed — no tokens received.</h1>';\n }\n <\/script>\n</body>\n</html>"))}catch(e){i.writeHead(500),i.end("Authentication failed"),s(),o.close(),n(e)}});o.on("request",async(e,r)=>{if("POST"===e.method&&"/token"===e.url){let i="";e.on("data",e=>{i+=e.toString()}),e.on("end",async()=>{try{const e=JSON.parse(i);await T({access_token:e.access_token,refresh_token:e.refresh_token,expires_at:Date.now()+1e3*(e.expires_in??3600),user_email:e.user_email}),r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0})),s(),o.close(),t({email:e.user_email??"authenticated"})}catch(e){r.writeHead(500),r.end("Failed to store credentials"),s(),o.close(),n(e)}})}}),o.listen(_t,"127.0.0.1",async()=>{process.stderr.write("\nOpening browser for ATOMS login...\n"),process.stderr.write(`If the browser doesn't open, visit:\n${r.toString()}\n\n`);try{const{default:e}=await import("open");await e(r.toString())}catch{process.stderr.write("Could not open browser automatically. Please open the URL above.\n")}}),i=setTimeout(()=>{o.close(),n(new Error("Login timed out after 60 seconds. Run 'npx @atoms-tech/atoms-mcp login' to try again."))},6e4)})}function bt(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Authorized</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #052e16;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 {\n font-size: 22px;\n font-weight: 600;\n margin-bottom: 8px;\n color: #22c55e;\n }\n .email {\n font-size: 14px;\n color: #a3a3a3;\n margin-bottom: 32px;\n }\n .permissions {\n text-align: left;\n background: #0a0a0a;\n border: 1px solid #262626;\n border-radius: 12px;\n padding: 20px;\n margin-bottom: 24px;\n }\n .permissions h3 {\n font-size: 12px;\n font-weight: 600;\n color: #737373;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 12px;\n }\n .perm-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 0;\n font-size: 14px;\n color: #d4d4d4;\n }\n .perm-item .dot {\n width: 6px;\n height: 6px;\n background: #22c55e;\n border-radius: 50%;\n flex-shrink: 0;\n }\n .closing {\n font-size: 13px;\n color: #525252;\n }\n .closing span { color: #a3a3a3; font-variant-numeric: tabular-nums; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>\n </svg>\n </div>\n <h1>Authorization Granted</h1>\n <p class="email">${e}</p>\n\n <div class="permissions">\n <h3>ATOMS MCP can now</h3>\n <div class="perm-item"><span class="dot"></span> Read your projects and requirements</div>\n <div class="perm-item"><span class="dot"></span> Create and edit requirements and test cases</div>\n <div class="perm-item"><span class="dot"></span> Record test results (pass/fail)</div>\n <div class="perm-item"><span class="dot"></span> Manage traceability relationships</div>\n <div class="perm-item"><span class="dot"></span> Export coverage reports and diagrams</div>\n </div>\n\n <p class="closing">This tab will close in <span id="countdown">5</span>s</p>\n </div>\n\n <script>\n let seconds = 5;\n const el = document.getElementById('countdown');\n const timer = setInterval(() => {\n seconds--;\n el.textContent = seconds;\n if (seconds <= 0) {\n clearInterval(timer);\n window.close();\n // If window.close() is blocked (not opened by script), update message\n setTimeout(() => {\n document.querySelector('.closing').textContent = 'You can close this tab now.';\n }, 500);\n }\n }, 1000);\n <\/script>\n</body>\n</html>`}function vt(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Failed</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #450a0a;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #ef4444; }\n .reason { font-size: 14px; color: #a3a3a3; line-height: 1.6; }\n .reason a { color: #7c3aed; text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class="reason">${e}</p>\n </div>\n</body>\n</html>`}x(yt,{login:()=>wt});var jt=j({"src/auth/login.ts"(){D(),z(),_t=19275,gt="/callback"}});ft();var xt=process.argv.slice(2)[0];(async function(){switch(xt){case"login":{const{login:e}=await Promise.resolve().then(()=>(jt(),yt));try{const{email:t}=await e();process.stderr.write(`\n ✓ Authenticated as ${t}\n`),process.stderr.write(" ✓ Credentials saved to ~/.atoms/credentials.json\n\n"),process.stderr.write(" Next steps:\n"),process.stderr.write(" $ claude mcp add atoms -- npx -y @atoms-tech/atoms-mcp\n"),process.stderr.write("\n Or add to Claude Desktop (Settings → MCP → Edit Config):\n"),process.stderr.write(' {\n "mcpServers": {\n "atoms": {\n "command": "npx",\n "args": ["-y", "@atoms-tech/atoms-mcp"]\n }\n }\n }\n\n')}catch(e){process.stderr.write(`\n ✗ ${e instanceof Error?e.message:String(e)}\n\n`),process.exit(1)}break}case"whoami":{const{getValidToken:e}=await Promise.resolve().then(()=>(J(),P));try{const{email:t,user_id:n}=await e();process.stderr.write(`\n ✓ Logged in as ${t}\n User ID: ${n}\n\n`)}catch(e){process.stderr.write("\n ✗ Not authenticated.\n Run: npx @atoms-tech/atoms-mcp login\n\n"),process.exit(1)}break}case"logout":{const{clearCredentials:e}=await Promise.resolve().then(()=>(D(),I));await e(),process.stderr.write("Logged out. Credentials removed.\n"),process.stderr.write("Run 'npx @atoms-tech/atoms-mcp login' to authenticate again.\n");break}case"--version":case"-v":process.stderr.write("@atoms-tech/atoms-mcp v0.1.0\n");break;case"--help":case"-h":process.stderr.write("ATOMS MCP Server — AI agent integration for requirements management\n\nUsage:\n npx @atoms-tech/atoms-mcp Start MCP server (stdio)\n npx @atoms-tech/atoms-mcp login Authenticate with ATOMS.tech\n npx @atoms-tech/atoms-mcp logout Switch account / clear credentials\n npx @atoms-tech/atoms-mcp whoami Show current user\n npx @atoms-tech/atoms-mcp --version Show version\n");break;default:{process.stderr.write("[atoms-mcp] Starting MCP server via stdio...\n");try{const{getValidToken:e}=await Promise.resolve().then(()=>(J(),P)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch{process.stderr.write("[atoms-mcp] No credentials found. Starting login flow...\n");try{const{login:e}=await Promise.resolve().then(()=>(jt(),yt)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch(e){process.stderr.write(`[atoms-mcp] Login failed: ${e instanceof Error?e.message:String(e)}\n[atoms-mcp] Starting server without auth. Tools will fail until you run: npx @atoms-tech/atoms-mcp login\n`)}}const e=new l.StdioServerTransport;await dt.connect(e),process.stderr.write("[atoms-mcp] MCP server running. Waiting for tool calls...\n")}}})().catch(e=>{process.stderr.write(`[atoms-mcp] Fatal: ${e instanceof Error?e.message:String(e)}\n`),process.exit(1)});
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import e,{readFile as t,mkdir as r,writeFile as n,unlink as i}from"fs/promises";import{homedir as s}from"os";import a,{join as o}from"path";import{createClient as c}from"@supabase/supabase-js";import{registerAppTool as d,registerAppResource as l,RESOURCE_MIME_TYPE as m}from"@modelcontextprotocol/ext-apps/server";import{URL as p,fileURLToPath as u}from"url";import{McpServer as h}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as _}from"zod";import{randomUUID as g,randomBytes as f,createHash as y}from"crypto";import{createServer as w}from"http";import{StdioServerTransport as b}from"@modelcontextprotocol/sdk/server/stdio.js";var v,j,x,S,I,k=Object.defineProperty,E=Object.getOwnPropertyNames,T=(e,t)=>function(){return e&&(t=(0,e[E(e)[0]])(e=0)),t},H=(e,t)=>{for(var r in t)k(e,r,{get:t[r],enumerable:!0})},C=T({"src/middleware/audit.ts"(){}}),O=T({"src/middleware/rate-limiter.ts"(){v=60,j=6e4,x=new Map}}),D={};async function U(){try{const e=await t(I,"utf-8"),r=JSON.parse(e);return r.access_token&&r.refresh_token?r:null}catch{return null}}async function M(e){await r(S,{recursive:!0,mode:448}),await n(I,JSON.stringify(e,null,2),{mode:384})}async function q(){const e=await U();return!!e&&e.expires_at>Date.now()+6e4}async function P(){try{await i(I)}catch{}}function A(){return I}H(D,{clearCredentials:()=>P,getCredentialsPath:()=>A,hasValidCredentials:()=>q,readCredentials:()=>U,writeCredentials:()=>M});var R,$,N,W=T({"src/auth/token-store.ts"(){S=o(s(),".atoms"),I=o(S,"credentials.json")}}),F=T({"src/config.ts"(){R="https://gmebjyhomsbvhrxffzre.supabase.co",$="sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH",N=process.env.ATOMS_APP_URL??"https://x.atoms.tech"}}),L={};async function z(){const e=process.env.ATOMS_ACCESS_TOKEN;if(e){const t=J(e);return{access_token:e,refresh_token:"",user_id:t.sub,email:t.email??""}}const t=await U();if(!t)throw new Error("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' to connect your ATOMS account.");if(t.expires_at>Date.now()+6e4){const e=J(t.access_token);return{access_token:t.access_token,refresh_token:t.refresh_token,user_id:e.sub,email:e.email??t.user_email??""}}process.stderr.write("[atoms-mcp] Refreshing access token...\n");const r=c(R,$),{data:n,error:i}=await r.auth.refreshSession({refresh_token:t.refresh_token});if(i||!n.session)throw new Error(`Token refresh failed: ${i?.message??"No session returned"}. Re-run 'npx @atoms-tech/atoms-mcp login'.`);const s=n.session;return await M({access_token:s.access_token,refresh_token:s.refresh_token,expires_at:Date.now()+1e3*(s.expires_in??3600),user_email:s.user?.email}),{access_token:s.access_token,refresh_token:s.refresh_token,user_id:s.user?.id??"",email:s.user?.email??""}}function J(e){const t=e.split(".");if(3!==t.length)throw new Error("Invalid JWT format");const r=t[1].replace(/-/g,"+").replace(/_/g,"/").padEnd(t[1].length+(4-t[1].length%4)%4,"="),n=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));if(!n.sub)throw new Error("JWT missing 'sub' claim");return n}H(L,{getValidToken:()=>z});var B,V,G,Q,Y=T({"src/auth/refresh.ts"(){W(),F()}});async function K(){if(B&&Q>Date.now()+6e4)return B;B&&process.stderr.write("[atoms-mcp] Token expiring, refreshing client...\n");const{access_token:e,user_id:t,email:r}=await z(),n=e.split(".");if(3===n.length)try{const e=JSON.parse(Buffer.from(n[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString());Q=1e3*(e.exp??0)}catch{Q=Date.now()+36e5}const i=c(R,$,{global:{headers:{Authorization:`Bearer ${e}`}}});return V=t,G=r,B=i}function X(){if(!V)throw new Error("Not authenticated. Call getClient() first.");return V}async function Z(e,t){const r=X(),{data:n,error:i}=await e.from("projects").select("org_id").eq("id",t).maybeSingle();if(i||!n)throw new Error(`Project '${t}' not found`);const{data:s,error:a}=await e.from("org_members").select("role").eq("user_id",r).eq("org_id",n.org_id).maybeSingle();if(a||!s){const{data:t}=await e.rpc("is_platform_admin");if(!0===t)return"admin";throw new Error("Not a member of this project's organization")}if("viewer"===s.role)throw new Error("VIEWER_ROLE");return s.role}var ee,te=T({"src/db/client.ts"(){Y(),F(),B=null,V=null,G=null,Q=0}});function re(e,t){return{status:"success",data:e,...t?{meta:t}:{}}}function ne(e,t,r){return{total_count:e,limit:t,offset:r,has_more:r+t<e}}function ie(e,t){return{status:"error",message:e,next_steps:t}}function se(e,t){return ie(`${e} '${t}' not found`,[`Verify the ${e.toLowerCase()} ID is correct`,"Use atoms_list_items or atoms_search to find valid IDs"])}function ae(e){return ie(`${e} role cannot modify project data`,["Contact your org admin to upgrade your role to editor","Use read-only tools (atoms_list_items, atoms_get_item, atoms_search) instead"])}function oe(e){return ie(`Validation error: ${e}`,["Check the parameter types and constraints in the tool description","Ensure all required parameters are provided"])}function ce(){return ie("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' first.",["Run: npx @atoms-tech/atoms-mcp login","Or set ATOMS_ACCESS_TOKEN environment variable"])}function de(e){return ie(`Database error: ${e}`,["This may be a temporary issue — try again","If the error persists, check that the project_id is valid"])}function le(e){const t=JSON.stringify(e,null,2);if(t.length>ee&&"success"===e.status&&Array.isArray(e.data)){const t=Math.max(1,Math.floor(e.data.length/2)),r={...e,data:e.data.slice(0,t),meta:{...e.meta,truncated:!0,truncation_message:`Response truncated from ${e.data.length} to ${t} items. Use 'offset' parameter or add filters to see more results.`}};return{content:[{type:"text",text:JSON.stringify(r,null,2)}],structuredContent:r}}return{content:[{type:"text",text:t}],structuredContent:e}}function me(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}],structuredContent:e,isError:!0}}var pe,ue=T({"src/tools/_base.ts"(){ee=25e3}});function he(t,r){const n=`ui://${r.toolName}/${r.htmlFile}`,i=function(e,t){const r=u(import.meta.url),n=a.dirname(r);return a.resolve(n,"apps","src","apps",e,t)}(r.appName,r.htmlFile),s=[...pe.resourceDomains,...r.extraResourceDomains??[]];d(t,r.toolName,{title:r.title,description:r.description,inputSchema:r.inputSchema,annotations:r.annotations,_meta:{ui:{resourceUri:n}}},r.handler),l(t,`${r.title} UI`,n,{mimeType:m},async()=>{const t=await e.readFile(i,"utf-8");return{contents:[{uri:n,mimeType:m,text:t,_meta:{ui:{csp:{resourceDomains:s}}}}]}})}var _e=T({"src/apps/register.ts"(){pe={resourceDomains:["fonts.googleapis.com","fonts.gstatic.com"]}}});async function ge(e,t,r){const{data:n,error:i}=await e.from("items").select("*").eq("id",r).eq("project_id",t).is("deleted_at",null).maybeSingle();if(i)throw new Error(i.message);return n}async function fe(e,t,r){const n="requirement"===r?"REQ":"test-case"===r?"TC":"note"===r?"NOTE":"TABLE",{data:i,error:s}=await e.rpc("next_item_id",{p_project_id:t,p_prefix:n});if(s)throw new Error(`ID generation failed: ${s.message}`);return i}var ye=T({"src/db/queries.ts"(){}}),we={};async function be(){try{const e=await K(),t=await async function(e){const{data:t,error:r}=await e.from("projects").select("id, name, description, org_id, created_at, organizations(name)").is("deleted_at",null).order("created_at",{ascending:!1});if(r)throw new Error(r.message);return(t??[]).map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.organizations?.name??"Unknown",created_at:e.created_at}))}(e);return le(re(t.map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.org_name,created_at:e.created_at}))))}catch(e){return me(de(e instanceof Error?e.message:"Unknown error"))}}H(we,{listProjectsHandler:()=>be});var ve=T({"src/tools/list-projects.ts"(){te(),ye(),ue()}}),je={};async function xe(e){try{const t=await K(),{items:r,totalCount:n}=await async function(e,t,r){let n=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).order("created_at",{ascending:!0}).range(r.offset,r.offset+r.limit-1);r.type&&(n=n.eq("type",r.type));const{data:i,error:s,count:a}=await n;if(s)throw new Error(s.message);let o=i??[];return r.domain&&(o=o.filter(e=>(e.data?.tags?.domains??[]).includes(r.domain))),r.level&&(o=o.filter(e=>e.data?.tags?.level===r.level)),{items:o,totalCount:r.domain||r.level?o.length:a??0}}(t,e.project_id,{type:e.type,domain:e.domain,level:e.level,limit:e.limit,offset:e.offset});return le(re(r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),ne(n,e.limit,e.offset)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(je,{listItemsHandler:()=>xe});var Se=T({"src/tools/list-items.ts"(){te(),ye(),ue()}}),Ie={};async function ke(e){try{const t=await K(),r=await ge(t,e.project_id,e.item_id);if(!r)return me(se("Item",e.item_id));const n=await async function(e,t,r){const{data:n,error:i}=await e.from("item_relationships").select("from_id, to_id, type").eq("project_id",t).or(`from_id.eq.${r},to_id.eq.${r}`);if(i)throw new Error(i.message);return n??[]}(t,e.project_id,e.item_id),i={parents:[],children:[],related:[],verified_by:[],verifies:[]};for(const t of n)if(t.from_id===e.item_id)t.type in i&&i[t.type].push(t.to_id);else{const e={parent:"children",child:"parents",verified_by:"verifies",verifies:"verified_by",related:"related"}[t.type]??t.type;e in i&&i[e].push(t.from_id)}let s=[];if("test-case"===r.type){const{data:r,error:n}=await t.from("test_results").select("result, run_by, run_at, note").eq("item_id",e.item_id).eq("project_id",e.project_id).order("run_at",{ascending:!1}).limit(20);!n&&r&&(s=r)}return le(re({id:r.id,type:r.type,title:r.title,summary:r.data?.summary,body:r.data?.body,tags:r.data?.tags??{domains:[]},ownership:r.data?.ownership??{primary:null,additional:[]},relationships:i,links:r.data?.links??[],status:r.data?.status,..."test-case"===r.type?{latest_result:s[0]?.result??"not-run",test_results:s}:{},metadata:{created_at:r.created_at,created_by:r.created_by,updated_at:r.updated_at,updated_by:r.updated_by,source_id:r.data?.metadata?.source_id}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Ie,{getItemHandler:()=>ke});var Ee=T({"src/tools/get-item.ts"(){te(),ye(),ue()}}),Te={};async function He(e){try{const t=await K(),{items:r,totalCount:n}=await async function(e,t,r,n){const i=r.trim();if(!i)return{items:[],totalCount:0};let s=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).or(`title.ilike.%${i}%,data->>body.ilike.%${i}%,data->>summary.ilike.%${i}%`).limit(n.limit);n.type&&(s=s.eq("type",n.type));const{data:a,error:o,count:c}=await s;if(o)throw new Error(o.message);return{items:a??[],totalCount:c??0}}(t,e.project_id,e.query,{type:e.type,limit:e.limit});return le(re(r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),ne(n,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Te,{searchHandler:()=>He});var Ce=T({"src/tools/search.ts"(){te(),ye(),ue()}}),Oe={};async function De(e){try{const t=await K(),{covered:r,uncovered:n,total:i}=await async function(e,t,r){const{data:n,error:i}=await e.from("items").select("*").eq("project_id",t).eq("type","requirement").is("deleted_at",null);if(i)throw new Error(i.message);const s=(n??[]).filter(e=>!(r.domain&&!(e.data?.tags?.domains??[]).includes(r.domain)||r.level&&e.data?.tags?.level!==r.level)),{data:a,error:o}=await e.from("item_relationships").select("from_id").eq("project_id",t).eq("type","verified_by");if(o)throw new Error(o.message);const c=new Set((a??[]).map(e=>e.from_id));return{covered:s.filter(e=>c.has(e.id)),uncovered:s.filter(e=>!c.has(e.id)),total:s.length}}(t,e.project_id,{domain:e.domain,level:e.level}),s=n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level}));return le(re({covered:r.length,uncovered:n.length,total:i,coverage_percent:i>0?Math.round(r.length/i*1e3)/10:100,uncovered_items:s}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Oe,{getCoverageHandler:()=>De});var Ue=T({"src/tools/get-coverage.ts"(){te(),ye(),ue()}}),Me={};async function qe(e){try{const t=await K(),r=await async function(e,t,r,n){const{data:i,error:s}=await e.from("change_history").select("*").eq("item_id",r).eq("project_id",t).order("changed_at",{ascending:!1}).limit(n);if(s)throw new Error(s.message);return i??[]}(t,e.project_id,e.item_id,e.limit),n=r.map(e=>{const t=[],r=e.old_data,n=e.new_data;if(r&&n){const e=new Set([...Object.keys(r),...Object.keys(n)]);for(const i of e)JSON.stringify(r[i])!==JSON.stringify(n[i])&&t.push(i)}else n?t.push("(created)"):r&&t.push("(deleted)");return{id:e.id,event_type:e.event_type,changed_by:e.changed_by??null,changed_at:e.changed_at,actor:e.actor??"user",session_id:e.session_id??null,fields_changed:t}});return le(re(n,ne(n.length,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Me,{getHistoryHandler:()=>qe});var Pe=T({"src/tools/get-history.ts"(){te(),ye(),ue()}}),Ae={};async function Re(e){try{const t=await K(),{data:r,error:n}=await t.from("items").select("id, title, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(n)throw new Error(n.message);if(!r||0===r.length)return le(re({mermaid:"graph TD\n empty[No items in project]"}));const{data:i,error:s}=await t.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(s)throw new Error(s.message);const a=new Map;for(const e of r)a.set(e.id,e);const o=["graph TD"],c=new Set,d=new Set,l=(new Set((i??[]).filter(e=>"child"===e.type).map(e=>e.to_id)),new Set((i??[]).filter(e=>"parent"===e.type).map(e=>e.from_id)));let m;m=e.root_item_id?[e.root_item_id]:r.filter(e=>!l.has(e.id)).filter(t=>!(!e.include_tests&&"test-case"===t.type)).map(e=>e.id);const p=m.map(e=>({id:e,depth:0}));for(;p.length>0;){const{id:t,depth:r}=p.shift();if(c.has(t)||r>e.depth)continue;c.add(t);const n=a.get(t);if(!n)continue;if(!e.include_tests&&"test-case"===n.type)continue;const s=$e(n.title),l="requirement"===n.type?`["${s}"]`:"test-case"===n.type?`(["${s}"])`:`("${s}")`;o.push(` ${t}${l}`);for(const n of i??[]){if(n.from_id!==t)continue;const i=a.get(n.to_id);if(!i)continue;if(!e.include_tests&&"test-case"===i.type)continue;const s=`${n.from_id}-${n.type}-${n.to_id}`;d.has(s)||(d.add(s),"child"===n.type?o.push(` ${t} --\x3e ${n.to_id}`):"verified_by"===n.type?o.push(` ${t} -.->|verified_by| ${n.to_id}`):"related"===n.type&&o.push(` ${t} -.- ${n.to_id}`),!c.has(n.to_id)&&r+1<=e.depth&&p.push({id:n.to_id,depth:r+1}))}}return le(re({mermaid:o.join("\n"),node_count:c.size,edge_count:d.size}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}function $e(e){return e.replace(/"/g,"'").replace(/[[\]{}()]/g,"").substring(0,50)}H(Ae,{exportMermaidHandler:()=>Re});var Ne=T({"src/tools/export-mermaid.ts"(){te(),ue()}}),We={};async function Fe(e){try{const t=await K(),r=X();try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const n=await fe(t,e.project_id,e.type),i={id:n,type:e.type,title:e.title,summary:e.summary,body:e.body,tags:{domains:e.domains??[],level:e.level},ownership:{primary:null,additional:[]},relationships:{parents:e.parent_ids??[],children:[],related:[]},links:[],metadata:{created_at:(new Date).toISOString(),created_by:r,updated_at:(new Date).toISOString(),updated_by:r}},{error:s}=await t.from("items").insert({id:n,project_id:e.project_id,type:e.type,title:e.title,data:i,created_by:r,updated_by:r}).select().single();if(s)throw new Error(s.message);if(e.parent_ids&&e.parent_ids.length>0){const r=e.parent_ids.map(t=>({from_id:n,to_id:t,type:"parent",project_id:e.project_id}));await t.from("item_relationships").insert(r).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: relationship sync failed: ${e.message}\n`)})}return await t.from("change_history").insert({item_id:n,project_id:e.project_id,changed_by:r,event_type:"created",old_data:null,new_data:i,actor:"mcp_claude",session_id:wt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),le(re({id:n,type:e.type,title:e.title,project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(We,{createItemHandler:()=>Fe});var Le=T({"src/tools/create-item.ts"(){te(),ye(),jt(),ue()}}),ze={};async function Je(e){try{const t=await K(),r=X();try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const n=await ge(t,e.project_id,e.item_id);if(!n)return me(se("Item",e.item_id));const i={...n.data},s={...n.data};void 0!==e.title&&(s.title=e.title),void 0!==e.body&&(s.body=e.body),void 0!==e.summary&&(s.summary=e.summary),void 0!==e.domains&&(s.tags={...s.tags,domains:e.domains}),void 0!==e.level&&(s.tags={...s.tags,level:e.level}),void 0!==e.status&&(s.status=e.status),s.metadata={...s.metadata,updated_at:(new Date).toISOString(),updated_by:r};const{error:a}=await t.from("items").update({title:e.title??n.title,data:s,updated_by:r}).eq("id",e.item_id).eq("project_id",e.project_id);if(a)throw new Error(a.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"updated",old_data:i,new_data:s,actor:"mcp_claude",session_id:wt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),le(re({id:e.item_id,title:s.title,type:n.type,updated_fields:Object.keys(e).filter(t=>"project_id"!==t&&"item_id"!==t&&void 0!==e[t])}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(ze,{updateItemHandler:()=>Je});var Be=T({"src/tools/update-item.ts"(){te(),ye(),jt(),ue()}}),Ve={};async function Ge(e){try{const t=await K(),r=X();try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const n=await ge(t,e.project_id,e.item_id);if(!n)return me(se("Item",e.item_id));const{error:i}=await t.from("items").update({deleted_at:(new Date).toISOString()}).eq("id",e.item_id).eq("project_id",e.project_id);if(i)throw new Error(i.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"deleted",old_data:n.data,new_data:null,actor:"mcp_claude",session_id:wt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),le(re({id:e.item_id,title:n.title,type:n.type,deleted:!0,message:`Item ${e.item_id} soft-deleted. It can still be found in audit logs.`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Ve,{deleteItemHandler:()=>Ge});var Qe=T({"src/tools/delete-item.ts"(){te(),ye(),jt(),ue()}}),Ye={};async function Ke(e){try{const t=await K(),r=X();if(e.from_id===e.to_id)return me(oe("Cannot create a relationship between an item and itself"));try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const n=await ge(t,e.project_id,e.from_id);if(!n)return me(se("Item",e.from_id));if(!await ge(t,e.project_id,e.to_id))return me(se("Item",e.to_id));if("add"===e.action){const{data:i}=await t.from("item_relationships").select("from_id").eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id).maybeSingle();if(!i){const{error:r}=await t.from("item_relationships").insert({from_id:e.from_id,to_id:e.to_id,type:e.type,project_id:e.project_id});if(r)throw new Error(r.message)}const s={...n.data},a="verified_by"===e.type||"verifies"===e.type?e.type:e.type+"s"=="parents"||"parent"===e.type?"parents":"child"===e.type?"children":"related";if(s.relationships){const t=s.relationships[a]??[];t.includes(e.to_id)||(s.relationships[a]=[...t,e.to_id])}await t.from("items").update({data:s,updated_by:r}).eq("id",e.from_id).eq("project_id",e.project_id)}else{const{error:i}=await t.from("item_relationships").delete().eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id);if(i)throw new Error(i.message);const s={...n.data},a="parent"===e.type?"parents":"child"===e.type?"children":e.type;if(s.relationships){const t=s.relationships[a]??[];s.relationships[a]=t.filter(t=>t!==e.to_id)}await t.from("items").update({data:s,updated_by:r}).eq("id",e.from_id).eq("project_id",e.project_id)}return await t.from("change_history").insert({item_id:e.from_id,project_id:e.project_id,changed_by:r,event_type:"updated",old_data:null,new_data:{relationship_change:{action:e.action,type:e.type,from:e.from_id,to:e.to_id}},actor:"mcp_claude",session_id:wt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),le(re({action:e.action,type:e.type,from_id:e.from_id,to_id:e.to_id,message:`Relationship ${"add"===e.action?"added":"removed"}: ${e.from_id} --[${e.type}]--\x3e ${e.to_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Ye,{linkItemsHandler:()=>Ke});var Xe=T({"src/tools/link-items.ts"(){te(),ye(),jt(),ue()}}),Ze={};async function et(e){try{if(!e.items||0===e.items.length)return me(oe("items array must contain at least 1 item"));if(e.items.length>100)return me(oe(`Maximum 100 items per call (received ${e.items.length}). Split into multiple calls.`));const t=await K(),r=X();try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const n=(new Date).toISOString(),i=wt(),s=[],a=[],o=[];for(let r=0;r<e.items.length;r++){const n=e.items[r];try{const i=await fe(t,e.project_id,n.type);o.push({itemId:i,input:n,index:r})}catch(e){a.push({index:r,title:n.title,error:`ID generation failed: ${e instanceof Error?e.message:String(e)}`})}}if(0===o.length)return le(re({created:0,items:[],errors:a}));const c=[],d=new Map;for(const{itemId:t,input:i}of o){const s={id:t,type:i.type,title:i.title,summary:i.summary,body:i.body,tags:{domains:i.domains??[],level:i.level},ownership:{primary:null,additional:[]},relationships:{parents:i.parent_id?[i.parent_id]:[],children:[],related:[]},links:[],metadata:{created_at:n,created_by:r,updated_at:n,updated_by:r}};d.set(t,s),c.push({id:t,project_id:e.project_id,type:i.type,title:i.title,data:s,created_by:r,updated_by:r})}const{error:l}=await t.from("items").insert(c);if(l)for(const{itemId:e,input:r,index:n}of o){const i=c.find(t=>t.id===e);if(!i)continue;const{error:o}=await t.from("items").insert(i);o?a.push({index:n,title:r.title,error:o.message}):s.push({id:e,title:r.title,type:r.type})}else for(const{itemId:e,input:t}of o)s.push({id:e,title:t.title,type:t.type});const m=o.filter(({input:e})=>e.parent_id).filter(({itemId:e})=>s.some(t=>t.id===e)).map(({itemId:t,input:r})=>({from_id:t,to_id:r.parent_id,type:"parent",project_id:e.project_id}));m.length>0&&await t.from("item_relationships").insert(m).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk relationship sync failed: ${e.message}\n`)});const p=s.map(t=>({item_id:t.id,project_id:e.project_id,changed_by:r,event_type:"created",old_data:null,new_data:d.get(t.id)??null,actor:"mcp_claude",session_id:i}));return p.length>0&&await t.from("change_history").insert(p).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk change_history log failed: ${e.message}\n`)}),le(re({created:s.length,items:s,errors:a.length>0?a:[],project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(Ze,{bulkImportHandler:()=>et});var tt=T({"src/tools/bulk-import.ts"(){te(),ye(),jt(),ue()}}),rt={};async function nt(e){try{const t=await K(),r=X(),n=G??"";try{await Z(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return me(ae("Viewer"));throw e}const i=await ge(t,e.project_id,e.item_id);if(!i)return me(se("Item",e.item_id));if("test-case"!==i.type)return me(oe(`Item '${e.item_id}' is a ${i.type}, not a test-case`));const s=(new Date).toISOString(),{error:a}=await t.from("test_results").insert({item_id:e.item_id,project_id:e.project_id,result:e.result,run_by:n||r,run_at:s,note:e.note??null});if(a)throw new Error(a.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:r,event_type:"test_result_recorded",old_data:null,new_data:{result:e.result,run_at:s,note:e.note},actor:"mcp_claude",session_id:wt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),le(re({item_id:e.item_id,result:e.result,run_at:s,run_by:n||r,note:e.note??null,message:`Test result '${e.result}' recorded for ${e.item_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(rt,{recordTestResultHandler:()=>nt});var it,st,at,ot,ct=T({"src/tools/record-test-result.ts"(){te(),ye(),jt(),ue()}}),dt={};async function lt(e){try{const t=Math.min(Math.max(e.depth??5,1),10),r=await K();if(!await ge(r,e.project_id,e.item_id))return me(se("Item",e.item_id));const{data:n,error:i}=await r.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(i)throw new Error(i.message);const{data:s,error:a}=await r.from("items").select("id, title, type").eq("project_id",e.project_id).is("deleted_at",null);if(a)throw new Error(a.message);const o=new Map;for(const e of s??[])o.set(e.id,{title:e.title,type:e.type});const c=new Map,d=(e,t,r)=>{c.has(e)||c.set(e,[]),c.get(e).push({neighbor:t,relType:r})},l=e.relationship_types?new Set(e.relationship_types):null;for(const t of n??[]){const r=t.type;if(("upstream"===e.direction||"both"===e.direction)&&(st.includes(r)&&(l&&!l.has(r)||d(t.from_id,t.to_id,r)),at.includes(r))){const e=ot[r]??r;l&&!l.has(e)||d(t.to_id,t.from_id,e)}if(("downstream"===e.direction||"both"===e.direction)&&(at.includes(r)&&(l&&!l.has(r)||d(t.from_id,t.to_id,r)),st.includes(r))){const e=ot[r]??r;l&&!l.has(e)||d(t.to_id,t.from_id,e)}"related"===r&&(l&&!l.has("related")||(d(t.from_id,t.to_id,"related"),d(t.to_id,t.from_id,"related")))}const m=new Set;m.add(e.item_id);const p=[],u=[],h=c.get(e.item_id)??[];for(const e of h)u.push({id:e.neighbor,depth:1,relType:e.relType});for(;u.length>0&&p.length<it;){const{id:e,depth:r,relType:n}=u.shift();if(m.has(e)||r>t)continue;m.add(e);const i=o.get(e);if(i&&(p.push({id:e,title:i.title,type:i.type,relationship:n,depth:r}),r<t)){const t=c.get(e)??[];for(const e of t)m.has(e.neighbor)||u.push({id:e.neighbor,depth:r+1,relType:e.relType})}}return le(re({root:e.item_id,direction:e.direction,depth_limit:t,items:p,total_count:p.length,...p.length>=it?{truncated:!0,truncation_message:`Results capped at ${it} items. Use a smaller depth or filter by relationship_types.`}:{}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(dt,{traceHandler:()=>lt});var mt=T({"src/tools/trace.ts"(){te(),ye(),ue(),it=200,st=["parent","verifies"],at=["child","verified_by"],ot={parent:"child",child:"parent",verifies:"verified_by",verified_by:"verifies",related:"related"}}}),pt={};async function ut(e){try{const t=await K(),{data:r,error:n}=await t.from("projects").select("id, name").eq("id",e.project_id).is("deleted_at",null).maybeSingle();if(n)throw new Error(n.message);if(!r)return me(se("Project",e.project_id));const{data:i,error:s}=await t.from("items").select("id, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(s)throw new Error(s.message);const a=i??[],o={requirements:0,test_cases:0,notes:0},c=[],d=[];for(const e of a)if("requirement"===e.type){o.requirements++;const t=e.data?.tags?.domains??[];d.push({id:e.id,domains:t})}else"test-case"===e.type?(o.test_cases++,c.push(e.id)):"note"===e.type&&o.notes++;const l={passed:0,failed:0,blocked:0,not_run:0};if(c.length>0){const{data:r,error:n}=await t.from("test_results").select("item_id, result, run_at").eq("project_id",e.project_id).order("run_at",{ascending:!1});if(n)throw new Error(n.message);const i=new Map;for(const e of r??[])i.has(e.item_id)||i.set(e.item_id,e.result);for(const e of c){const t=i.get(e);t&&"not-run"!==t?"passed"===t?l.passed++:"failed"===t?l.failed++:"blocked"===t?l.blocked++:l.not_run++:l.not_run++}}const{data:m,error:p}=await t.from("item_relationships").select("from_id").eq("project_id",e.project_id).eq("type","verified_by");if(p)throw new Error(p.message);const u=new Set((m??[]).map(e=>e.from_id)),h=d.filter(e=>u.has(e.id)).length,_=d.length,g={covered:h,uncovered:_-h,total:_,percent:_>0?Math.round(h/_*1e3)/10:100},f=new Map;for(const e of d){const t=e.domains.length>0?e.domains:["(untagged)"],r=u.has(e.id);for(const e of t){f.has(e)||f.set(e,{covered:0,total:0});const t=f.get(e);t.total++,r&&t.covered++}}const y=Array.from(f.entries()).sort((e,t)=>e[0].localeCompare(t[0])).map(([e,t])=>({domain:e,covered:t.covered,total:t.total,percent:t.total>0?Math.round(t.covered/t.total*1e3)/10:100})),w=new Date;w.setDate(w.getDate()-7);const{count:b,error:v}=await t.from("change_history").select("id",{count:"exact",head:!0}).eq("project_id",e.project_id).gte("changed_at",w.toISOString());if(v)throw new Error(v.message);return le(re({project_id:e.project_id,project_name:r.name,counts:o,test_status:l,coverage:g,coverage_by_domain:y,recent_changes:b??0,last_updated:(new Date).toISOString()}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?me(ce()):me(de(e instanceof Error?e.message:String(e)))}}H(pt,{projectSummaryHandler:()=>ut});var ht,_t,gt,ft=T({"src/tools/project-summary.ts"(){te(),ue()}});function yt(e,t){return async r=>{let n;try{await K(),n=X()}catch{return t(r)}const i=function(e){const t=parseInt(process.env.ATOMS_RATE_LIMIT_RPM??"",10)||v,r=Date.now(),n=r-j;let i=x.get(e);if(i||(i={timestamps:[]},x.set(e,i)),i.timestamps=i.timestamps.filter(e=>e>n),i.timestamps.length>=t){const e=i.timestamps[0]+j-r;return{allowed:!1,retryAfterSeconds:Math.ceil(e/1e3)}}return i.timestamps.push(r),{allowed:!0}}(n);if(!i.allowed)return me(ie("Rate limit exceeded",[`Wait ${i.retryAfterSeconds} seconds before making more requests`,"Reduce the frequency of tool calls"]));const s=function(){const e=performance.now();return()=>Math.round(performance.now()-e)}();let a,o="success";try{const e=await t(r),n=e;if(!0===n?.isError){o="error";const e=n?.content;if(e?.[0]?.text)try{a=JSON.parse(e[0].text).message}catch{}}return e}catch(e){throw o="error",a=e instanceof Error?e.message:String(e),e}finally{const t={tool_name:e,params:r,status:o,duration_ms:s(),error_msg:a,project_id:r.project_id,session_id:_t,client_name:gt};try{!function(e,t,r){const n={...r.params};delete n.access_token,delete n.refresh_token,delete n.password,e.from("mcp_audit_log").insert({user_id:t,tool_name:r.tool_name,params:n,status:r.status,duration_ms:r.duration_ms,error_msg:r.error_msg??null,project_id:r.project_id??null,session_id:r.session_id??null,client_name:r.client_name??null}).then(({error:e})=>{e&&process.stderr.write(`[audit] Failed to log: ${e.message}\n`)})}(await K(),n,t)}catch{}}}}function wt(){return _t}var bt,vt,jt=T({"src/server.ts"(){C(),O(),te(),ue(),_e(),ht=new h({name:"atoms-mcp-server",version:"0.1.0"}),_t=g(),gt=process.env.ATOMS_CLIENT_NAME??"unknown",ht.registerTool("atoms_list_projects",{title:"List ATOMS Projects",description:'List all projects the authenticated user has access to.\n\nThis is the ENTRY POINT tool. Call this first to discover project IDs,\nthen use those IDs with all other tools.\n\nArgs: None\n\nReturns:\n { status: "success", data: [{ id, name, description, org_id, created_at }] }\n\nExamples:\n - "What projects do I have?" → atoms_list_projects()\n - "Show my projects" → atoms_list_projects()\n\nWorkflow:\n atoms_list_projects → atoms_list_items(project_id) → atoms_get_item(project_id, item_id)',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_list_projects",async()=>{const{listProjectsHandler:e}=await Promise.resolve().then(()=>(ve(),we));return e()})),ht.registerTool("atoms_list_items",{title:"List ATOMS Items",description:'List items in an ATOMS project with optional type/domain/level filters.\n\nThis is the primary ENTRY POINT for discovering items. Use it to browse\nproject contents before drilling into specific items with atoms_get_item.\n\nArgs:\n - project_id (string, UUID): The project to list items from\n - type (string, optional): Filter: requirement|test-case|note|table\n - domain (string, optional): Filter by domain tag\n - level (string, optional): Filter by level (System, Subsystem, Component)\n - limit (number, optional): Max results, 1-200 (default 50)\n - offset (number, optional): Pagination offset (default 0)\n\nReturns:\n { status: "success", data: [{ id, title, type, status, domains, level }], meta: { total_count, limit, offset, has_more } }\n\nExamples:\n - "Show me all requirements" → atoms_list_items(project_id, type="requirement")\n - "What test cases exist?" → atoms_list_items(project_id, type="test-case")\n\nErrors:\n - "Project not found" → verify project_id\n - "Access denied" → check org membership',inputSchema:{project_id:_.string().uuid("Must be a valid project UUID").describe("UUID of the project"),type:_.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),domain:_.string().optional().describe("Filter by domain tag (e.g., 'Safety', 'Performance')"),level:_.string().optional().describe("Filter by level (System, Subsystem, Component)"),limit:_.number().int().min(1).max(200).default(50).describe("Max results (default 50, max 200)"),offset:_.number().int().min(0).default(0).describe("Pagination offset (default 0)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_list_items",async e=>{const{listItemsHandler:t}=await Promise.resolve().then(()=>(Se(),je));return t(e)})),ht.registerTool("atoms_get_item",{title:"Get ATOMS Item Details",description:'Get full details of a single item including relationships, test history, and ownership.\n\nUse after atoms_list_items or atoms_search to drill into a specific item.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID (e.g., "REQ-00001", "TC-00050")\n\nReturns:\n Full WorkItem with relationships, test runs, ownership, metadata.\n\nExamples:\n - "Show me requirement REQ-00001" → atoms_get_item(project_id, "REQ-00001")\n - "Get test case details" → atoms_get_item(project_id, "TC-00050")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID (e.g., REQ-00001)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_get_item",async e=>{const{getItemHandler:t}=await Promise.resolve().then(()=>(Ee(),Ie));return t(e)})),ht.registerTool("atoms_search",{title:"Search ATOMS Items",description:'Full-text search across items in a project. Searches title, body, and summary.\n\nAlternative entry point to atoms_list_items when you know what you\'re looking for.\n\nArgs:\n - project_id (string, UUID): The project to search in\n - query (string): Search text (max 1000 chars)\n - type (string, optional): Filter by type\n - limit (number, optional): Max results, 1-100 (default 25)\n\nReturns:\n Matching items with @basic fields, ranked by relevance.\n\nExamples:\n - "Find braking requirements" → atoms_search(project_id, "braking system")\n - "Search for safety tests" → atoms_search(project_id, "safety", type="test-case")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),query:_.string().min(1).max(1e3).describe("Search text"),type:_.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),limit:_.number().int().min(1).max(100).default(25).describe("Max results (default 25, max 100)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_search",async e=>{const{searchHandler:t}=await Promise.resolve().then(()=>(Ce(),Te));return t(e)})),he(ht,{appName:"coverage",htmlFile:"coverage-app.html",toolName:"atoms_get_coverage",title:"Get Test Coverage Report",description:'Find requirements without linked test cases (coverage gaps).\n\nEssential for compliance reporting in safety-critical industries.\n\nIn MCP App hosts (Claude, ChatGPT), renders a visual coverage heatmap.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to analyze\n - domain (string, optional): Filter by domain\n - level (string, optional): Filter by level\n\nReturns:\n { covered, uncovered, total, coverage_percent, uncovered_items: [...] }\n\nExamples:\n - "What\'s our test coverage?" → atoms_get_coverage(project_id)\n - "Safety coverage gaps?" → atoms_get_coverage(project_id, domain="Safety")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),domain:_.string().optional().describe("Filter by domain tag"),level:_.string().optional().describe("Filter by level")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:yt("atoms_get_coverage",async e=>{const{getCoverageHandler:t}=await Promise.resolve().then(()=>(Ue(),Oe));return t(e)})}),ht.registerTool("atoms_get_history",{title:"Get Item Change History",description:"Audit trail for an item — who changed what, when, and whether it was human or AI.\n\nShows actor attribution (user vs mcp_claude) and session grouping.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID\n - limit (number, optional): Max entries (default 20, max 100)\n\nReturns:\n Change entries with actor, session_id, event_type, changed_at, fields_changed.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID"),limit:_.number().int().min(1).max(100).default(20).describe("Max entries (default 20)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_get_history",async e=>{const{getHistoryHandler:t}=await Promise.resolve().then(()=>(Pe(),Me));return t(e)})),he(ht,{appName:"mermaid",htmlFile:"mermaid-app.html",toolName:"atoms_export_mermaid",title:"Export Mermaid Diagram",description:"Generate a Mermaid diagram of the requirement/test hierarchy.\n\nOutput is paste-ready for markdown docs. Shows parent-child relationships\nand requirement-to-test-case verification links.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive pan/zoom diagram.\nFalls back to Mermaid text for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to visualize\n - root_item_id (string, optional): Start node (default: all roots)\n - depth (number, optional): Max depth (default 3, max 10)\n - include_tests (boolean, optional): Show test case links (default true)\n\nReturns:\n Mermaid graph string.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),root_item_id:_.string().optional().describe("Root item ID to start from"),depth:_.number().int().min(1).max(10).default(3).describe("Max depth"),include_tests:_.boolean().default(!0).describe("Include test case links")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},extraResourceDomains:["cdn.jsdelivr.net"],handler:yt("atoms_export_mermaid",async e=>{const{exportMermaidHandler:t}=await Promise.resolve().then(()=>(Ne(),Ae));return t(e)})}),ht.registerTool("atoms_create_item",{title:"Create ATOMS Item",description:"Create a new requirement, test case, or note in a project.\n\nRequires editor or admin role. Changes are logged with AI actor attribution.\n\nArgs:\n - project_id (string, UUID): Target project\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_ids (string[], optional): Parent item IDs to link\n\nReturns:\n Created item with generated ID (e.g., \"REQ-00058\")\n\nSide effects:\n - Logs to change_history with actor='mcp_claude'\n - Creates relationships if parent_ids provided",inputSchema:{project_id:_.string().uuid().describe("UUID of the target project"),type:_.enum(["requirement","test-case","note"]).describe("Item type"),title:_.string().min(1).max(500).describe("Item title"),body:_.string().optional().describe("Rich text body"),summary:_.string().optional().describe("Brief summary"),domains:_.array(_.string()).optional().describe("Domain tags"),level:_.string().optional().describe("System|Subsystem|Component"),parent_ids:_.array(_.string()).optional().describe("Parent item IDs to link")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},yt("atoms_create_item",async e=>{const{createItemHandler:t}=await Promise.resolve().then(()=>(Le(),We));return t(e)})),ht.registerTool("atoms_update_item",{title:"Update ATOMS Item",description:"Update an existing item's fields. Only provided fields are changed.\n\nRequires editor or admin role. Logs old/new data diff to change history.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to update\n - title, body, summary, domains, level, status: Fields to update (all optional)\n\nReturns:\n Updated item.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID to update"),title:_.string().min(1).max(500).optional().describe("New title"),body:_.string().optional().describe("New body"),summary:_.string().optional().describe("New summary"),domains:_.array(_.string()).optional().describe("New domain tags"),level:_.string().optional().describe("New level"),status:_.enum(["passed","failed","blocked","not-run"]).optional().describe("Test case status")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_update_item",async e=>{const{updateItemHandler:t}=await Promise.resolve().then(()=>(Be(),ze));return t(e)})),ht.registerTool("atoms_delete_item",{title:"Delete ATOMS Item",description:"Soft-delete an item (sets deleted_at, preserves for audit trail).\n\nRequires editor or admin role. The item is never truly removed.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to delete\n\nReturns:\n Confirmation with deleted item summary.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID to soft-delete")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1}},yt("atoms_delete_item",async e=>{const{deleteItemHandler:t}=await Promise.resolve().then(()=>(Qe(),Ve));return t(e)})),ht.registerTool("atoms_link_items",{title:"Link ATOMS Items",description:"Add or remove relationships between items.\n\nSupports: parent, child, related, verifies, verified_by.\nUpdates both items' JSONB data and the shadow table.\n\nArgs:\n - project_id (string, UUID): Project containing both items\n - from_id (string): Source item\n - to_id (string): Target item\n - type (string): parent|child|related|verifies|verified_by\n - action (string): add|remove\n\nReturns:\n Updated relationship state for both items.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),from_id:_.string().min(1).max(20).describe("Source item ID"),to_id:_.string().min(1).max(20).describe("Target item ID"),type:_.enum(["parent","child","related","verifies","verified_by"]).describe("Relationship type"),action:_.enum(["add","remove"]).describe("Add or remove the relationship")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},yt("atoms_link_items",async e=>{const{linkItemsHandler:t}=await Promise.resolve().then(()=>(Xe(),Ye));return t(e)})),he(ht,{appName:"import",htmlFile:"import-app.html",toolName:"atoms_bulk_import",title:"Bulk Import ATOMS Items",description:"Bulk create multiple items in a single tool call. Essential for AI agents\nthat generate 20+ requirements at once.\n\nChecks write access once, generates sequential IDs, batch inserts all items,\nand reports per-item errors without aborting the entire batch.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive results table.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Target project\n - items (array): Up to 100 items to create, each with:\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_id (string, optional): Parent item ID to link\n\nReturns:\n { created: number, items: [{ id, title, type }], errors: [] }\n\nLimits:\n - Maximum 100 items per call\n - If any item fails, others still succeed — errors reported separately\n\nSide effects:\n - Logs to change_history with actor='mcp_claude' for each created item\n - Creates parent relationships for items with parent_id",inputSchema:{project_id:_.string().uuid().describe("UUID of the target project"),items:_.array(_.object({type:_.enum(["requirement","test-case","note"]).describe("Item type"),title:_.string().min(1).max(500).describe("Item title"),body:_.string().optional().describe("Rich text body"),summary:_.string().optional().describe("Brief summary"),domains:_.array(_.string()).optional().describe("Domain tags"),level:_.string().optional().describe("System|Subsystem|Component"),parent_id:_.string().optional().describe("Parent item ID to link")})).min(1).max(100).describe("Items to create (1-100)")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},handler:yt("atoms_bulk_import",async e=>{const{bulkImportHandler:t}=await Promise.resolve().then(()=>(tt(),Ze));return t(e)})}),ht.registerTool("atoms_record_test_result",{title:"Record Test Result",description:"Record a pass/fail/blocked result for a test case.\n\nAppends to the test results history. Use atoms_get_item to see all results.\n\nArgs:\n - project_id (string, UUID): Project containing the test case\n - item_id (string): Test case ID (e.g., \"TC-00001\")\n - result (string): passed|failed|blocked|not-run\n - note (string, optional): Reason or comment for this result\n\nReturns:\n Recorded result with timestamp.\n\nSide effects:\n - Appends to test_results table\n - Logs to change_history with actor='mcp_claude'",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Test case ID (e.g., TC-00001)"),result:_.enum(["passed","failed","blocked","not-run"]).describe("Test result"),note:_.string().max(1e3).optional().describe("Reason or comment for this result")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},yt("atoms_record_test_result",async e=>{const{recordTestResultHandler:t}=await Promise.resolve().then(()=>(ct(),rt));return t(e)})),he(ht,{appName:"trace",htmlFile:"trace-app.html",toolName:"atoms_trace",title:"Trace Item Relationships",description:'Walk the traceability graph from a starting item.\n\nAnswers questions like "which requirements does TC-00003 verify?" or\n"if I change REQ-00001, what\'s affected downstream?"\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive graph visualization.\nFalls back to flat list for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Starting item ID\n - direction (string): upstream|downstream|both\n - upstream: follow parent/verifies links (dependencies)\n - downstream: follow child/verified_by links (dependents)\n - both: trace in both directions\n - depth (number, optional): Max traversal depth (default 5, max 10)\n - relationship_types (string[], optional): Filter to specific types (parent, child, related, verifies, verified_by)\n\nReturns:\n { root, direction, items: [{ id, title, type, relationship, depth }], total_count }\n\nExamples:\n - "What does TC-00003 verify?" → atoms_trace(project_id, "TC-00003", "upstream")\n - "What\'s affected by REQ-00001?" → atoms_trace(project_id, "REQ-00001", "downstream")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Starting item ID"),direction:_.enum(["upstream","downstream","both"]).describe("Traversal direction"),depth:_.number().int().min(1).max(10).default(5).describe("Max traversal depth (default 5)"),relationship_types:_.array(_.enum(["parent","child","related","verifies","verified_by"])).optional().describe("Filter to specific relationship types")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:yt("atoms_trace",async e=>{const{traceHandler:t}=await Promise.resolve().then(()=>(mt(),dt));return t(e)})}),he(ht,{appName:"summary",htmlFile:"summary-app.html",toolName:"atoms_project_summary",title:"Project Compliance Summary",description:'One-call project health and compliance dashboard.\n\nReturns item counts by type, test execution status, requirement coverage\n(overall and by domain), and recent change activity.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive dashboard with charts.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project to summarize\n\nReturns:\n { project_name, counts, test_status, coverage, coverage_by_domain, recent_changes, last_updated }\n\nExamples:\n - "Give me a compliance snapshot" → atoms_project_summary(project_id)\n - "How\'s our test coverage?" → atoms_project_summary(project_id)',inputSchema:{project_id:_.string().uuid().describe("UUID of the project")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:yt("atoms_project_summary",async e=>{const{projectSummaryHandler:t}=await Promise.resolve().then(()=>(ft(),pt));return t(e)})})}}),xt={};async function St(){const e=f(32).toString("base64url"),t=y("sha256").update(e).digest("base64url"),r=`http://localhost:${bt}${vt}`,n=new p(`${N}/auth/mcp-consent`);return n.searchParams.set("redirect_uri",r),n.searchParams.set("code_challenge",t),n.searchParams.set("code_challenge_method","S256"),new Promise((t,r)=>{let i;function s(){clearTimeout(i)}const a=w(async(n,i)=>{const o=new p(n.url??"/",`http://localhost:${bt}`);if(o.pathname!==vt)return i.writeHead(404),void i.end("Not found");try{if("access_denied"===o.searchParams.get("error"))return i.writeHead(200,{"Content-Type":"text/html"}),i.end(kt("Authorization was cancelled.")),s(),a.close(),void r(new Error("Authorization cancelled by user."));const n=o.searchParams.get("code");if(n){const o=await fetch(`${R}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{"Content-Type":"application/json",apikey:$},body:JSON.stringify({auth_code:n,code_verifier:e})}),c=await o.json();if(!o.ok||!c.access_token){const e=c.error_description??c.msg??"Unknown error";return i.writeHead(200,{"Content-Type":"text/html"}),i.end(kt(`Code exchange failed: ${e}`)),s(),a.close(),void r(new Error(`Code exchange failed: ${e}`))}let d="authenticated";try{d=JSON.parse(Buffer.from(c.access_token.split(".")[1],"base64").toString()).email??d}catch{}const{createClient:l}=await import("@supabase/supabase-js"),m=l(R,$,{global:{headers:{Authorization:`Bearer ${c.access_token}`}}}),{data:p,error:u}=await m.from("org_members").select("org_id").limit(1);return u||!p||0===p.length?(i.writeHead(200,{"Content-Type":"text/html"}),i.end(kt('No ATOMS account found for this Google account.<br><br>Please sign up at <a href="https://x.atoms.tech">x.atoms.tech</a> first, then run this login command again.')),s(),a.close(),void r(new Error("No ATOMS account found. Sign up at x.atoms.tech first."))):(await M({access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Date.now()+1e3*(c.expires_in??3600),user_email:d}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(It(d)),s(),a.close(),void t({email:d}))}const c=o.searchParams.get("access_token"),d=o.searchParams.get("refresh_token"),l=parseInt(o.searchParams.get("expires_in")??"3600",10);c&&d?(await M({access_token:c,refresh_token:d,expires_at:Date.now()+1e3*l}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(It("authenticated")),s(),a.close(),t({email:"authenticated"})):(i.writeHead(200,{"Content-Type":"text/html"}),i.end("<!DOCTYPE html>\n<html>\n<head><title>ATOMS MCP Login</title></head>\n<body>\n <h1>Completing login...</h1>\n <script>\n const hash = window.location.hash.substring(1);\n const params = new URLSearchParams(hash);\n const data = {\n access_token: params.get('access_token'),\n refresh_token: params.get('refresh_token'),\n expires_in: params.get('expires_in'),\n user_email: ''\n };\n if (data.access_token) {\n // Decode JWT to get email\n try {\n const payload = JSON.parse(atob(data.access_token.split('.')[1]));\n data.user_email = payload.email || '';\n } catch(e) {}\n fetch('/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data)\n }).then(() => {\n document.body.innerHTML = '<h1>Login successful! You can close this tab.</h1>';\n }).catch(() => {\n document.body.innerHTML = '<h1>Login failed. Please try again.</h1>';\n });\n } else {\n document.body.innerHTML = '<h1>Login failed — no tokens received.</h1>';\n }\n <\/script>\n</body>\n</html>"))}catch(e){i.writeHead(500),i.end("Authentication failed"),s(),a.close(),r(e)}});a.on("request",async(e,n)=>{if("POST"===e.method&&"/token"===e.url){let i="";e.on("data",e=>{i+=e.toString()}),e.on("end",async()=>{try{const e=JSON.parse(i);await M({access_token:e.access_token,refresh_token:e.refresh_token,expires_at:Date.now()+1e3*(e.expires_in??3600),user_email:e.user_email}),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({ok:!0})),s(),a.close(),t({email:e.user_email??"authenticated"})}catch(e){n.writeHead(500),n.end("Failed to store credentials"),s(),a.close(),r(e)}})}}),a.listen(bt,"127.0.0.1",async()=>{process.stderr.write("\nOpening browser for ATOMS login...\n"),process.stderr.write(`If the browser doesn't open, visit:\n${n.toString()}\n\n`);try{const{default:e}=await import("open");await e(n.toString())}catch{process.stderr.write("Could not open browser automatically. Please open the URL above.\n")}}),i=setTimeout(()=>{a.close(),r(new Error("Login timed out after 60 seconds. Run 'npx @atoms-tech/atoms-mcp login' to try again."))},6e4)})}function It(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Authorized</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #052e16;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 {\n font-size: 22px;\n font-weight: 600;\n margin-bottom: 8px;\n color: #22c55e;\n }\n .email {\n font-size: 14px;\n color: #a3a3a3;\n margin-bottom: 32px;\n }\n .permissions {\n text-align: left;\n background: #0a0a0a;\n border: 1px solid #262626;\n border-radius: 12px;\n padding: 20px;\n margin-bottom: 24px;\n }\n .permissions h3 {\n font-size: 12px;\n font-weight: 600;\n color: #737373;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 12px;\n }\n .perm-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 0;\n font-size: 14px;\n color: #d4d4d4;\n }\n .perm-item .dot {\n width: 6px;\n height: 6px;\n background: #22c55e;\n border-radius: 50%;\n flex-shrink: 0;\n }\n .closing {\n font-size: 13px;\n color: #525252;\n }\n .closing span { color: #a3a3a3; font-variant-numeric: tabular-nums; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>\n </svg>\n </div>\n <h1>Authorization Granted</h1>\n <p class="email">${e}</p>\n\n <div class="permissions">\n <h3>ATOMS MCP can now</h3>\n <div class="perm-item"><span class="dot"></span> Read your projects and requirements</div>\n <div class="perm-item"><span class="dot"></span> Create and edit requirements and test cases</div>\n <div class="perm-item"><span class="dot"></span> Record test results (pass/fail)</div>\n <div class="perm-item"><span class="dot"></span> Manage traceability relationships</div>\n <div class="perm-item"><span class="dot"></span> Export coverage reports and diagrams</div>\n </div>\n\n <p class="closing">This tab will close in <span id="countdown">5</span>s</p>\n </div>\n\n <script>\n let seconds = 5;\n const el = document.getElementById('countdown');\n const timer = setInterval(() => {\n seconds--;\n el.textContent = seconds;\n if (seconds <= 0) {\n clearInterval(timer);\n window.close();\n // If window.close() is blocked (not opened by script), update message\n setTimeout(() => {\n document.querySelector('.closing').textContent = 'You can close this tab now.';\n }, 500);\n }\n }, 1000);\n <\/script>\n</body>\n</html>`}function kt(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Failed</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #450a0a;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #ef4444; }\n .reason { font-size: 14px; color: #a3a3a3; line-height: 1.6; }\n .reason a { color: #7c3aed; text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class="reason">${e}</p>\n </div>\n</body>\n</html>`}H(xt,{login:()=>St});var Et=T({"src/auth/login.ts"(){W(),F(),bt=19275,vt="/callback"}});jt();var Tt=process.argv.slice(2)[0];(async function(){switch(Tt){case"login":{const{login:e}=await Promise.resolve().then(()=>(Et(),xt));try{const{email:t}=await e();process.stderr.write(`\n ✓ Authenticated as ${t}\n`),process.stderr.write(" ✓ Credentials saved to ~/.atoms/credentials.json\n\n"),process.stderr.write(" Next steps:\n"),process.stderr.write(" $ claude mcp add atoms -- npx -y @atoms-tech/atoms-mcp\n"),process.stderr.write("\n Or add to Claude Desktop (Settings → MCP → Edit Config):\n"),process.stderr.write(' {\n "mcpServers": {\n "atoms": {\n "command": "npx",\n "args": ["-y", "@atoms-tech/atoms-mcp"]\n }\n }\n }\n\n')}catch(e){process.stderr.write(`\n ✗ ${e instanceof Error?e.message:String(e)}\n\n`),process.exit(1)}break}case"whoami":{const{getValidToken:e}=await Promise.resolve().then(()=>(Y(),L));try{const{email:t,user_id:r}=await e();process.stderr.write(`\n ✓ Logged in as ${t}\n User ID: ${r}\n\n`)}catch(e){process.stderr.write("\n ✗ Not authenticated.\n Run: npx @atoms-tech/atoms-mcp login\n\n"),process.exit(1)}break}case"logout":{const{clearCredentials:e}=await Promise.resolve().then(()=>(W(),D));await e(),process.stderr.write("Logged out. Credentials removed.\n"),process.stderr.write("Run 'npx @atoms-tech/atoms-mcp login' to authenticate again.\n");break}case"--version":case"-v":process.stderr.write("@atoms-tech/atoms-mcp v0.1.0\n");break;case"--help":case"-h":process.stderr.write("ATOMS MCP Server — AI agent integration for requirements management\n\nUsage:\n npx @atoms-tech/atoms-mcp Start MCP server (stdio)\n npx @atoms-tech/atoms-mcp login Authenticate with ATOMS.tech\n npx @atoms-tech/atoms-mcp logout Switch account / clear credentials\n npx @atoms-tech/atoms-mcp whoami Show current user\n npx @atoms-tech/atoms-mcp --version Show version\n");break;default:{process.stderr.write("[atoms-mcp] Starting MCP server via stdio...\n");try{const{getValidToken:e}=await Promise.resolve().then(()=>(Y(),L)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch{process.stderr.write("[atoms-mcp] No credentials found. Starting login flow...\n");try{const{login:e}=await Promise.resolve().then(()=>(Et(),xt)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch(e){process.stderr.write(`[atoms-mcp] Login failed: ${e instanceof Error?e.message:String(e)}\n[atoms-mcp] Starting server without auth. Tools will fail until you run: npx @atoms-tech/atoms-mcp login\n`)}}const e=new b;await ht.connect(e),process.stderr.write("[atoms-mcp] MCP server running. Waiting for tool calls...\n")}}})().catch(e=>{process.stderr.write(`[atoms-mcp] Fatal: ${e instanceof Error?e.message:String(e)}\n`),process.exit(1)});
2
+ import e,{readFile as t,mkdir as n,writeFile as r,unlink as i}from"fs/promises";import{homedir as a}from"os";import s,{join as o}from"path";import{createClient as c}from"@supabase/supabase-js";import{registerAppTool as d,registerAppResource as l,RESOURCE_MIME_TYPE as m}from"@modelcontextprotocol/ext-apps/server";import{URL as p,fileURLToPath as u}from"url";import{McpServer as h}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as _}from"zod";import{randomUUID as g,randomBytes as f,createHash as y}from"crypto";import{createServer as w}from"http";import{StdioServerTransport as b}from"@modelcontextprotocol/sdk/server/stdio.js";var v,j,x,S,k,I=Object.defineProperty,E=Object.getOwnPropertyNames,T=(e,t)=>function(){return e&&(t=(0,e[E(e)[0]])(e=0)),t},C=(e,t)=>{for(var n in t)I(e,n,{get:t[n],enumerable:!0})},H=T({"src/middleware/audit.ts"(){}}),O=T({"src/middleware/rate-limiter.ts"(){v=60,j=6e4,x=new Map}}),D={};async function M(){try{const e=await t(k,"utf-8"),n=JSON.parse(e);return n.access_token&&n.refresh_token?n:null}catch{return null}}async function P(e){await n(S,{recursive:!0,mode:448}),await r(k,JSON.stringify(e,null,2),{mode:384})}async function R(){const e=await M();return!!e&&e.expires_at>Date.now()+6e4}function U(e){return e.expires_at>Date.now()+6e4}async function q(){try{await i(k)}catch{}}function A(){return k}C(D,{clearCredentials:()=>q,getCredentialsPath:()=>A,hasValidCredentials:()=>R,isTokenValid:()=>U,readCredentials:()=>M,writeCredentials:()=>P});var N,$,W,F=T({"src/auth/token-store.ts"(){S=o(a(),".atoms"),k=o(S,"credentials.json")}}),L=T({"src/config.ts"(){N="https://gmebjyhomsbvhrxffzre.supabase.co",$="sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH",W=process.env.ATOMS_APP_URL??"https://x.atoms.tech"}}),z={};async function J(){const e=process.env.ATOMS_ACCESS_TOKEN;if(e){const t=V(e);return{access_token:e,refresh_token:"",user_id:t.sub,email:t.email??""}}const t=await M();if(!t)throw new Error("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' to connect your ATOMS account.");if(t.expires_at>Date.now()+6e4){const e=V(t.access_token);return{access_token:t.access_token,refresh_token:t.refresh_token,user_id:e.sub,email:e.email??t.user_email??""}}process.stderr.write("[atoms-mcp] Refreshing access token...\n");const n=c(N,$),{data:r,error:i}=await n.auth.refreshSession({refresh_token:t.refresh_token});if(i||!r.session)throw new Error(`Token refresh failed: ${i?.message??"No session returned"}. Re-run 'npx @atoms-tech/atoms-mcp login'.`);const a=r.session;return await P({access_token:a.access_token,refresh_token:a.refresh_token,expires_at:Date.now()+1e3*(a.expires_in??3600),user_email:a.user?.email}),{access_token:a.access_token,refresh_token:a.refresh_token,user_id:a.user?.id??"",email:a.user?.email??""}}function V(e){const t=e.split(".");if(3!==t.length)throw new Error("Invalid JWT format");const n=t[1].replace(/-/g,"+").replace(/_/g,"/").padEnd(t[1].length+(4-t[1].length%4)%4,"="),r=JSON.parse(Buffer.from(n,"base64").toString("utf-8"));if(!r.sub)throw new Error("JWT missing 'sub' claim");return r}C(z,{getValidToken:()=>J});var B,G,Q,Y,K=T({"src/auth/refresh.ts"(){F(),L()}});async function X(){if(B&&Y>Date.now()+6e4)return B;B&&process.stderr.write("[atoms-mcp] Token expiring, refreshing client...\n");const{access_token:e,user_id:t,email:n}=await J(),r=e.split(".");if(3===r.length)try{const e=JSON.parse(Buffer.from(r[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString());Y=1e3*(e.exp??0)}catch{Y=Date.now()+36e5}const i=c(N,$,{global:{headers:{Authorization:`Bearer ${e}`}}});return G=t,Q=n,B=i}function Z(){if(!G)throw new Error("Not authenticated. Call getClient() first.");return G}async function ee(e,t){const n=Z(),{data:r,error:i}=await e.from("projects").select("org_id").eq("id",t).maybeSingle();if(i||!r)throw new Error(`Project '${t}' not found`);const{data:a,error:s}=await e.from("org_members").select("role").eq("user_id",n).eq("org_id",r.org_id).maybeSingle();if(s||!a){const{data:t}=await e.rpc("is_platform_admin");if(!0===t)return"admin";throw new Error("Not a member of this project's organization")}if("viewer"===a.role)throw new Error("VIEWER_ROLE");return a.role}var te,ne=T({"src/db/client.ts"(){K(),L(),B=null,G=null,Q=null,Y=0}});function re(e,t){return{status:"success",data:e,...t?{meta:t}:{}}}function ie(e,t,n){return{total_count:e,limit:t,offset:n,has_more:n+t<e}}function ae(e,t){return{status:"error",message:e,next_steps:t}}function se(e,t){return ae(`${e} '${t}' not found`,[`Verify the ${e.toLowerCase()} ID is correct`,"Use atoms_list_items or atoms_search to find valid IDs"])}function oe(e){return ae(`${e} role cannot modify project data`,["Contact your org admin to upgrade your role to editor","Use read-only tools (atoms_list_items, atoms_get_item, atoms_search) instead"])}function ce(e){return ae(`Validation error: ${e}`,["Check the parameter types and constraints in the tool description","Ensure all required parameters are provided"])}function de(){return ae("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' first.",["Run: npx @atoms-tech/atoms-mcp login","Or set ATOMS_ACCESS_TOKEN environment variable"])}function le(e){return ae(`Database error: ${e}`,["This may be a temporary issue — try again","If the error persists, check that the project_id is valid"])}function me(e){const t=JSON.stringify(e,null,2);if(t.length>te&&"success"===e.status&&Array.isArray(e.data)){const t=Math.max(1,Math.floor(e.data.length/2)),n={...e,data:e.data.slice(0,t),meta:{...e.meta,truncated:!0,truncation_message:`Response truncated from ${e.data.length} to ${t} items. Use 'offset' parameter or add filters to see more results.`}};return{content:[{type:"text",text:JSON.stringify(n,null,2)}],structuredContent:n}}return{content:[{type:"text",text:t}],structuredContent:e}}function pe(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}],structuredContent:e,isError:!0}}var ue,he=T({"src/tools/_base.ts"(){te=25e3}});function _e(t,n){const r=`ui://${n.toolName}/${n.htmlFile}`,i=function(e,t){const n=u(import.meta.url),r=s.dirname(n);return s.resolve(r,"apps","src","apps",e,t)}(n.appName,n.htmlFile),a=[...ue.resourceDomains,...n.extraResourceDomains??[]];d(t,n.toolName,{title:n.title,description:n.description,inputSchema:n.inputSchema,annotations:n.annotations,_meta:{ui:{resourceUri:r}}},n.handler),l(t,`${n.title} UI`,r,{mimeType:m},async()=>{const t=await e.readFile(i,"utf-8");return{contents:[{uri:r,mimeType:m,text:t,_meta:{ui:{csp:{resourceDomains:a}}}}]}})}var ge=T({"src/apps/register.ts"(){ue={resourceDomains:["fonts.googleapis.com","fonts.gstatic.com"]}}});async function fe(e,t,n){const{data:r,error:i}=await e.from("items").select("*").eq("id",n).eq("project_id",t).is("deleted_at",null).maybeSingle();if(i)throw new Error(i.message);return r}async function ye(e,t,n){const r="requirement"===n?"REQ":"test-case"===n?"TC":"note"===n?"NOTE":"TABLE",{data:i,error:a}=await e.rpc("next_item_id",{p_project_id:t,p_prefix:r});if(a)throw new Error(`ID generation failed: ${a.message}`);return i}var we=T({"src/db/queries.ts"(){}}),be={};async function ve(){try{const e=await X(),t=await async function(e){const{data:t,error:n}=await e.from("projects").select("id, name, description, org_id, created_at, organizations(name)").is("deleted_at",null).order("created_at",{ascending:!1});if(n)throw new Error(n.message);return(t??[]).map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.organizations?.name??"Unknown",created_at:e.created_at}))}(e);return me(re(t.map(e=>({id:e.id,name:e.name,description:e.description,org_id:e.org_id,org_name:e.org_name,created_at:e.created_at}))))}catch(e){return pe(le(e instanceof Error?e.message:"Unknown error"))}}C(be,{listProjectsHandler:()=>ve});var je=T({"src/tools/list-projects.ts"(){ne(),we(),he()}}),xe={};async function Se(e){try{const t=await X(),{items:n,totalCount:r}=await async function(e,t,n){let r=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).order("created_at",{ascending:!0}).range(n.offset,n.offset+n.limit-1);n.type&&(r=r.eq("type",n.type));const{data:i,error:a,count:s}=await r;if(a)throw new Error(a.message);let o=i??[];return n.domain&&(o=o.filter(e=>(e.data?.tags?.domains??[]).includes(n.domain))),n.level&&(o=o.filter(e=>e.data?.tags?.level===n.level)),{items:o,totalCount:n.domain||n.level?o.length:s??0}}(t,e.project_id,{type:e.type,domain:e.domain,level:e.level,limit:e.limit,offset:e.offset});return me(re(n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),ie(r,e.limit,e.offset)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(xe,{listItemsHandler:()=>Se});var ke=T({"src/tools/list-items.ts"(){ne(),we(),he()}}),Ie={};async function Ee(e){try{const t=await X(),n=await fe(t,e.project_id,e.item_id);if(!n)return pe(se("Item",e.item_id));const r=await async function(e,t,n){const{data:r,error:i}=await e.from("item_relationships").select("from_id, to_id, type").eq("project_id",t).or(`from_id.eq.${n},to_id.eq.${n}`);if(i)throw new Error(i.message);return r??[]}(t,e.project_id,e.item_id),i={parents:[],children:[],related:[],verified_by:[],verifies:[]};for(const t of r)if(t.from_id===e.item_id)t.type in i&&i[t.type].push(t.to_id);else{const e={parent:"children",child:"parents",verified_by:"verifies",verifies:"verified_by",related:"related"}[t.type]??t.type;e in i&&i[e].push(t.from_id)}let a=[];if("test-case"===n.type){const{data:n,error:r}=await t.from("test_results").select("result, run_by, run_at, note").eq("item_id",e.item_id).eq("project_id",e.project_id).order("run_at",{ascending:!1}).limit(20);!r&&n&&(a=n)}return me(re({id:n.id,type:n.type,title:n.title,summary:n.data?.summary,body:n.data?.body,tags:n.data?.tags??{domains:[]},ownership:n.data?.ownership??{primary:null,additional:[]},relationships:i,links:n.data?.links??[],status:n.data?.status,..."test-case"===n.type?{latest_result:a[0]?.result??"not-run",test_results:a}:{},metadata:{created_at:n.created_at,created_by:n.created_by,updated_at:n.updated_at,updated_by:n.updated_by,source_id:n.data?.metadata?.source_id}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Ie,{getItemHandler:()=>Ee});var Te=T({"src/tools/get-item.ts"(){ne(),we(),he()}}),Ce={};async function He(e){try{const t=await X(),{items:n,totalCount:r}=await async function(e,t,n,r){const i=n.trim();if(!i)return{items:[],totalCount:0};let a=e.from("items").select("*",{count:"exact"}).eq("project_id",t).is("deleted_at",null).or(`title.ilike.%${i}%,data->>body.ilike.%${i}%,data->>summary.ilike.%${i}%`).limit(r.limit);r.type&&(a=a.eq("type",r.type));const{data:s,error:o,count:c}=await a;if(o)throw new Error(o.message);return{items:s??[],totalCount:c??0}}(t,e.project_id,e.query,{type:e.type,limit:e.limit});return me(re(n.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level})),ie(r,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Ce,{searchHandler:()=>He});var Oe=T({"src/tools/search.ts"(){ne(),we(),he()}}),De={};async function Me(e){try{const t=await X(),{covered:n,uncovered:r,total:i}=await async function(e,t,n){const{data:r,error:i}=await e.from("items").select("*").eq("project_id",t).eq("type","requirement").is("deleted_at",null);if(i)throw new Error(i.message);const a=(r??[]).filter(e=>!(n.domain&&!(e.data?.tags?.domains??[]).includes(n.domain)||n.level&&e.data?.tags?.level!==n.level)),{data:s,error:o}=await e.from("item_relationships").select("from_id").eq("project_id",t).eq("type","verified_by");if(o)throw new Error(o.message);const c=new Set((s??[]).map(e=>e.from_id));return{covered:a.filter(e=>c.has(e.id)),uncovered:a.filter(e=>!c.has(e.id)),total:a.length}}(t,e.project_id,{domain:e.domain,level:e.level}),a=r.map(e=>({id:e.id,title:e.title,type:e.type,status:e.data?.status,domains:e.data?.tags?.domains??[],level:e.data?.tags?.level}));return me(re({covered:n.length,uncovered:r.length,total:i,coverage_percent:i>0?Math.round(n.length/i*1e3)/10:100,uncovered_items:a}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(De,{getCoverageHandler:()=>Me});var Pe=T({"src/tools/get-coverage.ts"(){ne(),we(),he()}}),Re={};async function Ue(e){try{const t=await X(),n=await async function(e,t,n,r){const{data:i,error:a}=await e.from("change_history").select("*").eq("item_id",n).eq("project_id",t).order("changed_at",{ascending:!1}).limit(r);if(a)throw new Error(a.message);return i??[]}(t,e.project_id,e.item_id,e.limit),r=n.map(e=>{const t=[],n=e.old_data,r=e.new_data;if(n&&r){const e=new Set([...Object.keys(n),...Object.keys(r)]);for(const i of e)JSON.stringify(n[i])!==JSON.stringify(r[i])&&t.push(i)}else r?t.push("(created)"):n&&t.push("(deleted)");return{id:e.id,event_type:e.event_type,changed_by:e.changed_by??null,changed_at:e.changed_at,actor:e.actor??"user",session_id:e.session_id??null,fields_changed:t}});return me(re(r,ie(r.length,e.limit,0)))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Re,{getHistoryHandler:()=>Ue});var qe=T({"src/tools/get-history.ts"(){ne(),we(),he()}}),Ae={};async function Ne(e){try{const t=await X(),{data:n,error:r}=await t.from("items").select("id, title, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(r)throw new Error(r.message);if(!n||0===n.length)return me(re({mermaid:"graph TD\n empty[No items in project]"}));const{data:i,error:a}=await t.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(a)throw new Error(a.message);const s=new Map;for(const e of n)s.set(e.id,e);const o=["graph TD"],c=new Set,d=new Set,l=(new Set((i??[]).filter(e=>"child"===e.type).map(e=>e.to_id)),new Set((i??[]).filter(e=>"parent"===e.type).map(e=>e.from_id)));let m;m=e.root_item_id?[e.root_item_id]:n.filter(e=>!l.has(e.id)).filter(t=>!(!e.include_tests&&"test-case"===t.type)).map(e=>e.id);const p=m.map(e=>({id:e,depth:0}));for(;p.length>0;){const{id:t,depth:n}=p.shift();if(c.has(t)||n>e.depth)continue;c.add(t);const r=s.get(t);if(!r)continue;if(!e.include_tests&&"test-case"===r.type)continue;const a=$e(r.title),l="requirement"===r.type?`["${a}"]`:"test-case"===r.type?`(["${a}"])`:`("${a}")`;o.push(` ${t}${l}`);for(const r of i??[]){if(r.from_id!==t)continue;const i=s.get(r.to_id);if(!i)continue;if(!e.include_tests&&"test-case"===i.type)continue;const a=`${r.from_id}-${r.type}-${r.to_id}`;d.has(a)||(d.add(a),"child"===r.type?o.push(` ${t} --\x3e ${r.to_id}`):"verified_by"===r.type?o.push(` ${t} -.->|verified_by| ${r.to_id}`):"related"===r.type&&o.push(` ${t} -.- ${r.to_id}`),!c.has(r.to_id)&&n+1<=e.depth&&p.push({id:r.to_id,depth:n+1}))}}return me(re({mermaid:o.join("\n"),node_count:c.size,edge_count:d.size}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}function $e(e){return e.replace(/"/g,"'").replace(/[[\]{}()]/g,"").substring(0,50)}C(Ae,{exportMermaidHandler:()=>Ne});var We=T({"src/tools/export-mermaid.ts"(){ne(),he()}}),Fe={};async function Le(e){try{const t=await X(),n=Z();try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const r=await ye(t,e.project_id,e.type),i={id:r,type:e.type,title:e.title,summary:e.summary,body:e.body,tags:{domains:e.domains??[],level:e.level},ownership:{primary:null,additional:[]},relationships:{parents:e.parent_ids??[],children:[],related:[]},links:[],metadata:{created_at:(new Date).toISOString(),created_by:n,updated_at:(new Date).toISOString(),updated_by:n}},{error:a}=await t.from("items").insert({id:r,project_id:e.project_id,type:e.type,title:e.title,data:i,created_by:n,updated_by:n}).select().single();if(a)throw new Error(a.message);if(e.parent_ids&&e.parent_ids.length>0){const n=e.parent_ids.map(t=>({from_id:r,to_id:t,type:"parent",project_id:e.project_id}));await t.from("item_relationships").insert(n).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: relationship sync failed: ${e.message}\n`)})}return await t.from("change_history").insert({item_id:r,project_id:e.project_id,changed_by:n,event_type:"created",old_data:null,new_data:i,actor:"mcp_claude",session_id:bt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),me(re({id:r,type:e.type,title:e.title,project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Fe,{createItemHandler:()=>Le});var ze=T({"src/tools/create-item.ts"(){ne(),we(),xt(),he()}}),Je={};async function Ve(e){try{const t=await X(),n=Z();try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const r=await fe(t,e.project_id,e.item_id);if(!r)return pe(se("Item",e.item_id));const i={...r.data},a={...r.data};void 0!==e.title&&(a.title=e.title),void 0!==e.body&&(a.body=e.body),void 0!==e.summary&&(a.summary=e.summary),void 0!==e.domains&&(a.tags={...a.tags,domains:e.domains}),void 0!==e.level&&(a.tags={...a.tags,level:e.level}),void 0!==e.status&&(a.status=e.status),a.metadata={...a.metadata,updated_at:(new Date).toISOString(),updated_by:n};const{error:s}=await t.from("items").update({title:e.title??r.title,data:a,updated_by:n}).eq("id",e.item_id).eq("project_id",e.project_id);if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"updated",old_data:i,new_data:a,actor:"mcp_claude",session_id:bt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),me(re({id:e.item_id,title:a.title,type:r.type,updated_fields:Object.keys(e).filter(t=>"project_id"!==t&&"item_id"!==t&&void 0!==e[t])}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Je,{updateItemHandler:()=>Ve});var Be=T({"src/tools/update-item.ts"(){ne(),we(),xt(),he()}}),Ge={};async function Qe(e){try{const t=await X(),n=Z();try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const r=await fe(t,e.project_id,e.item_id);if(!r)return pe(se("Item",e.item_id));const{error:i}=await t.from("items").update({deleted_at:(new Date).toISOString()}).eq("id",e.item_id).eq("project_id",e.project_id);if(i)throw new Error(i.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"deleted",old_data:r.data,new_data:null,actor:"mcp_claude",session_id:bt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),me(re({id:e.item_id,title:r.title,type:r.type,deleted:!0,message:`Item ${e.item_id} soft-deleted. It can still be found in audit logs.`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Ge,{deleteItemHandler:()=>Qe});var Ye=T({"src/tools/delete-item.ts"(){ne(),we(),xt(),he()}}),Ke={};async function Xe(e){try{const t=await X(),n=Z();if(e.from_id===e.to_id)return pe(ce("Cannot create a relationship between an item and itself"));try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const r=await fe(t,e.project_id,e.from_id);if(!r)return pe(se("Item",e.from_id));if(!await fe(t,e.project_id,e.to_id))return pe(se("Item",e.to_id));if("add"===e.action){const{data:i}=await t.from("item_relationships").select("from_id").eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id).maybeSingle();if(!i){const{error:n}=await t.from("item_relationships").insert({from_id:e.from_id,to_id:e.to_id,type:e.type,project_id:e.project_id});if(n)throw new Error(n.message)}const a={...r.data},s="verified_by"===e.type||"verifies"===e.type?e.type:e.type+"s"=="parents"||"parent"===e.type?"parents":"child"===e.type?"children":"related";if(a.relationships){const t=a.relationships[s]??[];t.includes(e.to_id)||(a.relationships[s]=[...t,e.to_id])}await t.from("items").update({data:a,updated_by:n}).eq("id",e.from_id).eq("project_id",e.project_id)}else{const{error:i}=await t.from("item_relationships").delete().eq("from_id",e.from_id).eq("to_id",e.to_id).eq("type",e.type).eq("project_id",e.project_id);if(i)throw new Error(i.message);const a={...r.data},s="parent"===e.type?"parents":"child"===e.type?"children":e.type;if(a.relationships){const t=a.relationships[s]??[];a.relationships[s]=t.filter(t=>t!==e.to_id)}await t.from("items").update({data:a,updated_by:n}).eq("id",e.from_id).eq("project_id",e.project_id)}return await t.from("change_history").insert({item_id:e.from_id,project_id:e.project_id,changed_by:n,event_type:"updated",old_data:null,new_data:{relationship_change:{action:e.action,type:e.type,from:e.from_id,to:e.to_id}},actor:"mcp_claude",session_id:bt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),me(re({action:e.action,type:e.type,from_id:e.from_id,to_id:e.to_id,message:`Relationship ${"add"===e.action?"added":"removed"}: ${e.from_id} --[${e.type}]--\x3e ${e.to_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(Ke,{linkItemsHandler:()=>Xe});var Ze=T({"src/tools/link-items.ts"(){ne(),we(),xt(),he()}}),et={};async function tt(e){try{if(!e.items||0===e.items.length)return pe(ce("items array must contain at least 1 item"));if(e.items.length>100)return pe(ce(`Maximum 100 items per call (received ${e.items.length}). Split into multiple calls.`));const t=await X(),n=Z();try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const r=(new Date).toISOString(),i=bt(),a=[],s=[],o=[];for(let n=0;n<e.items.length;n++){const r=e.items[n];try{const i=await ye(t,e.project_id,r.type);o.push({itemId:i,input:r,index:n})}catch(e){s.push({index:n,title:r.title,error:`ID generation failed: ${e instanceof Error?e.message:String(e)}`})}}if(0===o.length)return me(re({created:0,items:[],errors:s}));const c=[],d=new Map;for(const{itemId:t,input:i}of o){const a={id:t,type:i.type,title:i.title,summary:i.summary,body:i.body,tags:{domains:i.domains??[],level:i.level},ownership:{primary:null,additional:[]},relationships:{parents:i.parent_id?[i.parent_id]:[],children:[],related:[]},links:[],metadata:{created_at:r,created_by:n,updated_at:r,updated_by:n}};d.set(t,a),c.push({id:t,project_id:e.project_id,type:i.type,title:i.title,data:a,created_by:n,updated_by:n})}const{error:l}=await t.from("items").insert(c);if(l)for(const{itemId:e,input:n,index:r}of o){const i=c.find(t=>t.id===e);if(!i)continue;const{error:o}=await t.from("items").insert(i);o?s.push({index:r,title:n.title,error:o.message}):a.push({id:e,title:n.title,type:n.type})}else for(const{itemId:e,input:t}of o)a.push({id:e,title:t.title,type:t.type});const m=o.filter(({input:e})=>e.parent_id).filter(({itemId:e})=>a.some(t=>t.id===e)).map(({itemId:t,input:n})=>({from_id:t,to_id:n.parent_id,type:"parent",project_id:e.project_id}));m.length>0&&await t.from("item_relationships").insert(m).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk relationship sync failed: ${e.message}\n`)});const p=a.map(t=>({item_id:t.id,project_id:e.project_id,changed_by:n,event_type:"created",old_data:null,new_data:d.get(t.id)??null,actor:"mcp_claude",session_id:i}));return p.length>0&&await t.from("change_history").insert(p).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: bulk change_history log failed: ${e.message}\n`)}),me(re({created:a.length,items:a,errors:s.length>0?s:[],project_id:e.project_id}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(et,{bulkImportHandler:()=>tt});var nt=T({"src/tools/bulk-import.ts"(){ne(),we(),xt(),he()}}),rt={};async function it(e){try{const t=await X(),n=Z(),r=Q??"";try{await ee(t,e.project_id)}catch(e){if(e instanceof Error&&"VIEWER_ROLE"===e.message)return pe(oe("Viewer"));throw e}const i=await fe(t,e.project_id,e.item_id);if(!i)return pe(se("Item",e.item_id));if("test-case"!==i.type)return pe(ce(`Item '${e.item_id}' is a ${i.type}, not a test-case`));const a=(new Date).toISOString(),{error:s}=await t.from("test_results").insert({item_id:e.item_id,project_id:e.project_id,result:e.result,run_by:r||n,run_at:a,note:e.note??null});if(s)throw new Error(s.message);return await t.from("change_history").insert({item_id:e.item_id,project_id:e.project_id,changed_by:n,event_type:"test_result_recorded",old_data:null,new_data:{result:e.result,run_at:a,note:e.note},actor:"mcp_claude",session_id:bt()}).then(({error:e})=>{e&&process.stderr.write(`[atoms-mcp] Warning: change_history log failed: ${e.message}\n`)}),me(re({item_id:e.item_id,result:e.result,run_at:a,run_by:r||n,note:e.note??null,message:`Test result '${e.result}' recorded for ${e.item_id}`}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(rt,{recordTestResultHandler:()=>it});var at,st,ot,ct,dt=T({"src/tools/record-test-result.ts"(){ne(),we(),xt(),he()}}),lt={};async function mt(e){try{const t=Math.min(Math.max(e.depth??5,1),10),n=await X();if(!await fe(n,e.project_id,e.item_id))return pe(se("Item",e.item_id));const{data:r,error:i}=await n.from("item_relationships").select("from_id, to_id, type").eq("project_id",e.project_id);if(i)throw new Error(i.message);const{data:a,error:s}=await n.from("items").select("id, title, type").eq("project_id",e.project_id).is("deleted_at",null);if(s)throw new Error(s.message);const o=new Map;for(const e of a??[])o.set(e.id,{title:e.title,type:e.type});const c=new Map,d=(e,t,n)=>{c.has(e)||c.set(e,[]),c.get(e).push({neighbor:t,relType:n})},l=e.relationship_types?new Set(e.relationship_types):null;for(const t of r??[]){const n=t.type;if(("upstream"===e.direction||"both"===e.direction)&&(st.includes(n)&&(l&&!l.has(n)||d(t.from_id,t.to_id,n)),ot.includes(n))){const e=ct[n]??n;l&&!l.has(e)||d(t.to_id,t.from_id,e)}if(("downstream"===e.direction||"both"===e.direction)&&(ot.includes(n)&&(l&&!l.has(n)||d(t.from_id,t.to_id,n)),st.includes(n))){const e=ct[n]??n;l&&!l.has(e)||d(t.to_id,t.from_id,e)}"related"===n&&(l&&!l.has("related")||(d(t.from_id,t.to_id,"related"),d(t.to_id,t.from_id,"related")))}const m=new Set;m.add(e.item_id);const p=[],u=[],h=c.get(e.item_id)??[];for(const e of h)u.push({id:e.neighbor,depth:1,relType:e.relType});for(;u.length>0&&p.length<at;){const{id:e,depth:n,relType:r}=u.shift();if(m.has(e)||n>t)continue;m.add(e);const i=o.get(e);if(i&&(p.push({id:e,title:i.title,type:i.type,relationship:r,depth:n}),n<t)){const t=c.get(e)??[];for(const e of t)m.has(e.neighbor)||u.push({id:e.neighbor,depth:n+1,relType:e.relType})}}return me(re({root:e.item_id,direction:e.direction,depth_limit:t,items:p,total_count:p.length,...p.length>=at?{truncated:!0,truncation_message:`Results capped at ${at} items. Use a smaller depth or filter by relationship_types.`}:{}}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(lt,{traceHandler:()=>mt});var pt=T({"src/tools/trace.ts"(){ne(),we(),he(),at=200,st=["parent","verifies"],ot=["child","verified_by"],ct={parent:"child",child:"parent",verifies:"verified_by",verified_by:"verifies",related:"related"}}}),ut={};async function ht(e){try{const t=await X(),{data:n,error:r}=await t.from("projects").select("id, name").eq("id",e.project_id).is("deleted_at",null).maybeSingle();if(r)throw new Error(r.message);if(!n)return pe(se("Project",e.project_id));const{data:i,error:a}=await t.from("items").select("id, type, data").eq("project_id",e.project_id).is("deleted_at",null);if(a)throw new Error(a.message);const s=i??[],o={requirements:0,test_cases:0,notes:0},c=[],d=[];for(const e of s)if("requirement"===e.type){o.requirements++;const t=e.data?.tags?.domains??[];d.push({id:e.id,domains:t})}else"test-case"===e.type?(o.test_cases++,c.push(e.id)):"note"===e.type&&o.notes++;const l={passed:0,failed:0,blocked:0,not_run:0};if(c.length>0){const{data:n,error:r}=await t.from("test_results").select("item_id, result, run_at").eq("project_id",e.project_id).order("run_at",{ascending:!1});if(r)throw new Error(r.message);const i=new Map;for(const e of n??[])i.has(e.item_id)||i.set(e.item_id,e.result);for(const e of c){const t=i.get(e);t&&"not-run"!==t?"passed"===t?l.passed++:"failed"===t?l.failed++:"blocked"===t?l.blocked++:l.not_run++:l.not_run++}}const{data:m,error:p}=await t.from("item_relationships").select("from_id").eq("project_id",e.project_id).eq("type","verified_by");if(p)throw new Error(p.message);const u=new Set((m??[]).map(e=>e.from_id)),h=d.filter(e=>u.has(e.id)).length,_=d.length,g={covered:h,uncovered:_-h,total:_,percent:_>0?Math.round(h/_*1e3)/10:100},f=new Map;for(const e of d){const t=e.domains.length>0?e.domains:["(untagged)"],n=u.has(e.id);for(const e of t){f.has(e)||f.set(e,{covered:0,total:0});const t=f.get(e);t.total++,n&&t.covered++}}const y=Array.from(f.entries()).sort((e,t)=>e[0].localeCompare(t[0])).map(([e,t])=>({domain:e,covered:t.covered,total:t.total,percent:t.total>0?Math.round(t.covered/t.total*1e3)/10:100})),w=new Date;w.setDate(w.getDate()-7);const{count:b,error:v}=await t.from("change_history").select("id",{count:"exact",head:!0}).eq("project_id",e.project_id).gte("changed_at",w.toISOString());if(v)throw new Error(v.message);return me(re({project_id:e.project_id,project_name:n.name,counts:o,test_status:l,coverage:g,coverage_by_domain:y,recent_changes:b??0,last_updated:(new Date).toISOString()}))}catch(e){return e instanceof Error&&e.message.includes("Not authenticated")?pe(de()):pe(le(e instanceof Error?e.message:String(e)))}}C(ut,{projectSummaryHandler:()=>ht});var _t,gt,ft,yt=T({"src/tools/project-summary.ts"(){ne(),he()}});function wt(e,t){return async n=>{let r;try{await X(),r=Z()}catch{return t(n)}const i=function(e){const t=parseInt(process.env.ATOMS_RATE_LIMIT_RPM??"",10)||v,n=Date.now(),r=n-j;let i=x.get(e);if(i||(i={timestamps:[]},x.set(e,i)),i.timestamps=i.timestamps.filter(e=>e>r),i.timestamps.length>=t){const e=i.timestamps[0]+j-n;return{allowed:!1,retryAfterSeconds:Math.ceil(e/1e3)}}return i.timestamps.push(n),{allowed:!0}}(r);if(!i.allowed)return pe(ae("Rate limit exceeded",[`Wait ${i.retryAfterSeconds} seconds before making more requests`,"Reduce the frequency of tool calls"]));const a=function(){const e=performance.now();return()=>Math.round(performance.now()-e)}();let s,o="success";try{const e=await t(n),r=e;if(!0===r?.isError){o="error";const e=r?.content;if(e?.[0]?.text)try{s=JSON.parse(e[0].text).message}catch{}}return e}catch(e){throw o="error",s=e instanceof Error?e.message:String(e),e}finally{const t={tool_name:e,params:n,status:o,duration_ms:a(),error_msg:s,project_id:n.project_id,session_id:gt,client_name:ft};try{!function(e,t,n){const r={...n.params};delete r.access_token,delete r.refresh_token,delete r.password,e.from("mcp_audit_log").insert({user_id:t,tool_name:n.tool_name,params:r,status:n.status,duration_ms:n.duration_ms,error_msg:n.error_msg??null,project_id:n.project_id??null,session_id:n.session_id??null,client_name:n.client_name??null}).then(({error:e})=>{e&&process.stderr.write(`[audit] Failed to log: ${e.message}\n`)})}(await X(),r,t)}catch{}}}}function bt(){return gt}var vt,jt,xt=T({"src/server.ts"(){H(),O(),ne(),he(),ge(),_t=new h({name:"atoms-mcp-server",version:"0.1.0"}),gt=g(),ft=process.env.ATOMS_CLIENT_NAME??"unknown",_t.registerTool("atoms_status",{title:"ATOMS MCP Health Check",description:'Check the health and authentication status of the ATOMS MCP server.\n\nCall this FIRST if other atoms tools fail or return auth errors.\nThis tool works WITHOUT authentication — it checks whether credentials\nexist and are valid, and tells the user exactly what to do if not.\n\nArgs: None\n\nReturns:\n { status: "authenticated"|"not_authenticated"|"expired", email, message, next_steps }\n\nExamples:\n - "Is ATOMS MCP working?" → atoms_status()\n - "Why are atoms tools failing?" → atoms_status()',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},async()=>{try{const{readCredentials:e,isTokenValid:t}=await Promise.resolve().then(()=>(F(),D)),n=await e();if(!n)return{content:[{type:"text",text:JSON.stringify({status:"not_authenticated",message:"No ATOMS credentials found. You need to login first.",next_steps:["1. Open a terminal on your machine","2. Run: npx @atoms-tech/atoms-mcp login","3. Complete the login in your browser","4. Restart Claude Desktop (quit and reopen)","5. Try your query again"]},null,2)}]};if(!t(n))try{const{getValidToken:e}=await Promise.resolve().then(()=>(K(),z)),{email:t}=await e();return{content:[{type:"text",text:JSON.stringify({status:"authenticated",email:t,message:"Token was expired but has been refreshed. You're good to go!"},null,2)}]}}catch{return{content:[{type:"text",text:JSON.stringify({status:"expired",email:n.user_email??"unknown",message:"Your session has expired and could not be refreshed.",next_steps:["1. Open a terminal on your machine","2. Run: npx @atoms-tech/atoms-mcp logout","3. Run: npx @atoms-tech/atoms-mcp login","4. Complete the login in your browser","5. Restart Claude Desktop (quit and reopen)"]},null,2)}]}}return{content:[{type:"text",text:JSON.stringify({status:"authenticated",email:n.user_email??"unknown",message:"ATOMS MCP is connected and authenticated. All tools are ready."},null,2)}]}}catch(e){return{content:[{type:"text",text:JSON.stringify({status:"error",message:`Health check failed: ${e instanceof Error?e.message:String(e)}`,next_steps:["1. Open a terminal","2. Run: npx @atoms-tech/atoms-mcp login","3. Restart Claude Desktop"]},null,2)}]}}}),_t.registerTool("atoms_list_projects",{title:"List ATOMS Projects",description:'List all projects the authenticated user has access to.\n\nThis is the ENTRY POINT tool. Call this first to discover project IDs,\nthen use those IDs with all other tools.\n\nArgs: None\n\nReturns:\n { status: "success", data: [{ id, name, description, org_id, created_at }] }\n\nExamples:\n - "What projects do I have?" → atoms_list_projects()\n - "Show my projects" → atoms_list_projects()\n\nWorkflow:\n atoms_list_projects → atoms_list_items(project_id) → atoms_get_item(project_id, item_id)',inputSchema:{},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_list_projects",async()=>{const{listProjectsHandler:e}=await Promise.resolve().then(()=>(je(),be));return e()})),_t.registerTool("atoms_list_items",{title:"List ATOMS Items",description:'List items in an ATOMS project with optional type/domain/level filters.\n\nThis is the primary ENTRY POINT for discovering items. Use it to browse\nproject contents before drilling into specific items with atoms_get_item.\n\nArgs:\n - project_id (string, UUID): The project to list items from\n - type (string, optional): Filter: requirement|test-case|note|table\n - domain (string, optional): Filter by domain tag\n - level (string, optional): Filter by level (System, Subsystem, Component)\n - limit (number, optional): Max results, 1-200 (default 50)\n - offset (number, optional): Pagination offset (default 0)\n\nReturns:\n { status: "success", data: [{ id, title, type, status, domains, level }], meta: { total_count, limit, offset, has_more } }\n\nExamples:\n - "Show me all requirements" → atoms_list_items(project_id, type="requirement")\n - "What test cases exist?" → atoms_list_items(project_id, type="test-case")\n\nErrors:\n - "Project not found" → verify project_id\n - "Access denied" → check org membership',inputSchema:{project_id:_.string().uuid("Must be a valid project UUID").describe("UUID of the project"),type:_.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),domain:_.string().optional().describe("Filter by domain tag (e.g., 'Safety', 'Performance')"),level:_.string().optional().describe("Filter by level (System, Subsystem, Component)"),limit:_.number().int().min(1).max(200).default(50).describe("Max results (default 50, max 200)"),offset:_.number().int().min(0).default(0).describe("Pagination offset (default 0)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_list_items",async e=>{const{listItemsHandler:t}=await Promise.resolve().then(()=>(ke(),xe));return t(e)})),_t.registerTool("atoms_get_item",{title:"Get ATOMS Item Details",description:'Get full details of a single item including relationships, test history, and ownership.\n\nUse after atoms_list_items or atoms_search to drill into a specific item.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID (e.g., "REQ-00001", "TC-00050")\n\nReturns:\n Full WorkItem with relationships, test runs, ownership, metadata.\n\nExamples:\n - "Show me requirement REQ-00001" → atoms_get_item(project_id, "REQ-00001")\n - "Get test case details" → atoms_get_item(project_id, "TC-00050")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID (e.g., REQ-00001)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_get_item",async e=>{const{getItemHandler:t}=await Promise.resolve().then(()=>(Te(),Ie));return t(e)})),_t.registerTool("atoms_search",{title:"Search ATOMS Items",description:'Full-text search across items in a project. Searches title, body, and summary.\n\nAlternative entry point to atoms_list_items when you know what you\'re looking for.\n\nArgs:\n - project_id (string, UUID): The project to search in\n - query (string): Search text (max 1000 chars)\n - type (string, optional): Filter by type\n - limit (number, optional): Max results, 1-100 (default 25)\n\nReturns:\n Matching items with @basic fields, ranked by relevance.\n\nExamples:\n - "Find braking requirements" → atoms_search(project_id, "braking system")\n - "Search for safety tests" → atoms_search(project_id, "safety", type="test-case")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),query:_.string().min(1).max(1e3).describe("Search text"),type:_.enum(["requirement","test-case","note","table"]).optional().describe("Filter by item type"),limit:_.number().int().min(1).max(100).default(25).describe("Max results (default 25, max 100)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_search",async e=>{const{searchHandler:t}=await Promise.resolve().then(()=>(Oe(),Ce));return t(e)})),_e(_t,{appName:"coverage",htmlFile:"coverage-app.html",toolName:"atoms_get_coverage",title:"Get Test Coverage Report",description:'Find requirements without linked test cases (coverage gaps).\n\nEssential for compliance reporting in safety-critical industries.\n\nIn MCP App hosts (Claude, ChatGPT), renders a visual coverage heatmap.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to analyze\n - domain (string, optional): Filter by domain\n - level (string, optional): Filter by level\n\nReturns:\n { covered, uncovered, total, coverage_percent, uncovered_items: [...] }\n\nExamples:\n - "What\'s our test coverage?" → atoms_get_coverage(project_id)\n - "Safety coverage gaps?" → atoms_get_coverage(project_id, domain="Safety")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),domain:_.string().optional().describe("Filter by domain tag"),level:_.string().optional().describe("Filter by level")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:wt("atoms_get_coverage",async e=>{const{getCoverageHandler:t}=await Promise.resolve().then(()=>(Pe(),De));return t(e)})}),_t.registerTool("atoms_get_history",{title:"Get Item Change History",description:"Audit trail for an item — who changed what, when, and whether it was human or AI.\n\nShows actor attribution (user vs mcp_claude) and session grouping.\n\nArgs:\n - project_id (string, UUID): The project containing the item\n - item_id (string): Item ID\n - limit (number, optional): Max entries (default 20, max 100)\n\nReturns:\n Change entries with actor, session_id, event_type, changed_at, fields_changed.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID"),limit:_.number().int().min(1).max(100).default(20).describe("Max entries (default 20)")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_get_history",async e=>{const{getHistoryHandler:t}=await Promise.resolve().then(()=>(qe(),Re));return t(e)})),_e(_t,{appName:"mermaid",htmlFile:"mermaid-app.html",toolName:"atoms_export_mermaid",title:"Export Mermaid Diagram",description:"Generate a Mermaid diagram of the requirement/test hierarchy.\n\nOutput is paste-ready for markdown docs. Shows parent-child relationships\nand requirement-to-test-case verification links.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive pan/zoom diagram.\nFalls back to Mermaid text for non-UI clients.\n\nArgs:\n - project_id (string, UUID): The project to visualize\n - root_item_id (string, optional): Start node (default: all roots)\n - depth (number, optional): Max depth (default 3, max 10)\n - include_tests (boolean, optional): Show test case links (default true)\n\nReturns:\n Mermaid graph string.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),root_item_id:_.string().optional().describe("Root item ID to start from"),depth:_.number().int().min(1).max(10).default(3).describe("Max depth"),include_tests:_.boolean().default(!0).describe("Include test case links")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},extraResourceDomains:["cdn.jsdelivr.net"],handler:wt("atoms_export_mermaid",async e=>{const{exportMermaidHandler:t}=await Promise.resolve().then(()=>(We(),Ae));return t(e)})}),_t.registerTool("atoms_create_item",{title:"Create ATOMS Item",description:"Create a new requirement, test case, or note in a project.\n\nRequires editor or admin role. Changes are logged with AI actor attribution.\n\nArgs:\n - project_id (string, UUID): Target project\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_ids (string[], optional): Parent item IDs to link\n\nReturns:\n Created item with generated ID (e.g., \"REQ-00058\")\n\nSide effects:\n - Logs to change_history with actor='mcp_claude'\n - Creates relationships if parent_ids provided",inputSchema:{project_id:_.string().uuid().describe("UUID of the target project"),type:_.enum(["requirement","test-case","note"]).describe("Item type"),title:_.string().min(1).max(500).describe("Item title"),body:_.string().optional().describe("Rich text body"),summary:_.string().optional().describe("Brief summary"),domains:_.array(_.string()).optional().describe("Domain tags"),level:_.string().optional().describe("System|Subsystem|Component"),parent_ids:_.array(_.string()).optional().describe("Parent item IDs to link")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},wt("atoms_create_item",async e=>{const{createItemHandler:t}=await Promise.resolve().then(()=>(ze(),Fe));return t(e)})),_t.registerTool("atoms_update_item",{title:"Update ATOMS Item",description:"Update an existing item's fields. Only provided fields are changed.\n\nRequires editor or admin role. Logs old/new data diff to change history.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to update\n - title, body, summary, domains, level, status: Fields to update (all optional)\n\nReturns:\n Updated item.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID to update"),title:_.string().min(1).max(500).optional().describe("New title"),body:_.string().optional().describe("New body"),summary:_.string().optional().describe("New summary"),domains:_.array(_.string()).optional().describe("New domain tags"),level:_.string().optional().describe("New level"),status:_.enum(["passed","failed","blocked","not-run"]).optional().describe("Test case status")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_update_item",async e=>{const{updateItemHandler:t}=await Promise.resolve().then(()=>(Be(),Je));return t(e)})),_t.registerTool("atoms_delete_item",{title:"Delete ATOMS Item",description:"Soft-delete an item (sets deleted_at, preserves for audit trail).\n\nRequires editor or admin role. The item is never truly removed.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Item to delete\n\nReturns:\n Confirmation with deleted item summary.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Item ID to soft-delete")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1}},wt("atoms_delete_item",async e=>{const{deleteItemHandler:t}=await Promise.resolve().then(()=>(Ye(),Ge));return t(e)})),_t.registerTool("atoms_link_items",{title:"Link ATOMS Items",description:"Add or remove relationships between items.\n\nSupports: parent, child, related, verifies, verified_by.\nUpdates both items' JSONB data and the shadow table.\n\nArgs:\n - project_id (string, UUID): Project containing both items\n - from_id (string): Source item\n - to_id (string): Target item\n - type (string): parent|child|related|verifies|verified_by\n - action (string): add|remove\n\nReturns:\n Updated relationship state for both items.",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),from_id:_.string().min(1).max(20).describe("Source item ID"),to_id:_.string().min(1).max(20).describe("Target item ID"),type:_.enum(["parent","child","related","verifies","verified_by"]).describe("Relationship type"),action:_.enum(["add","remove"]).describe("Add or remove the relationship")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},wt("atoms_link_items",async e=>{const{linkItemsHandler:t}=await Promise.resolve().then(()=>(Ze(),Ke));return t(e)})),_e(_t,{appName:"import",htmlFile:"import-app.html",toolName:"atoms_bulk_import",title:"Bulk Import ATOMS Items",description:"Bulk create multiple items in a single tool call. Essential for AI agents\nthat generate 20+ requirements at once.\n\nChecks write access once, generates sequential IDs, batch inserts all items,\nand reports per-item errors without aborting the entire batch.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive results table.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Target project\n - items (array): Up to 100 items to create, each with:\n - type (string): requirement|test-case|note\n - title (string): Item title (max 500 chars)\n - body (string, optional): Rich text body\n - summary (string, optional): Brief summary\n - domains (string[], optional): Domain tags\n - level (string, optional): System|Subsystem|Component\n - parent_id (string, optional): Parent item ID to link\n\nReturns:\n { created: number, items: [{ id, title, type }], errors: [] }\n\nLimits:\n - Maximum 100 items per call\n - If any item fails, others still succeed — errors reported separately\n\nSide effects:\n - Logs to change_history with actor='mcp_claude' for each created item\n - Creates parent relationships for items with parent_id",inputSchema:{project_id:_.string().uuid().describe("UUID of the target project"),items:_.array(_.object({type:_.enum(["requirement","test-case","note"]).describe("Item type"),title:_.string().min(1).max(500).describe("Item title"),body:_.string().optional().describe("Rich text body"),summary:_.string().optional().describe("Brief summary"),domains:_.array(_.string()).optional().describe("Domain tags"),level:_.string().optional().describe("System|Subsystem|Component"),parent_id:_.string().optional().describe("Parent item ID to link")})).min(1).max(100).describe("Items to create (1-100)")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1},handler:wt("atoms_bulk_import",async e=>{const{bulkImportHandler:t}=await Promise.resolve().then(()=>(nt(),et));return t(e)})}),_t.registerTool("atoms_record_test_result",{title:"Record Test Result",description:"Record a pass/fail/blocked result for a test case.\n\nAppends to the test results history. Use atoms_get_item to see all results.\n\nArgs:\n - project_id (string, UUID): Project containing the test case\n - item_id (string): Test case ID (e.g., \"TC-00001\")\n - result (string): passed|failed|blocked|not-run\n - note (string, optional): Reason or comment for this result\n\nReturns:\n Recorded result with timestamp.\n\nSide effects:\n - Appends to test_results table\n - Logs to change_history with actor='mcp_claude'",inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Test case ID (e.g., TC-00001)"),result:_.enum(["passed","failed","blocked","not-run"]).describe("Test result"),note:_.string().max(1e3).optional().describe("Reason or comment for this result")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!1}},wt("atoms_record_test_result",async e=>{const{recordTestResultHandler:t}=await Promise.resolve().then(()=>(dt(),rt));return t(e)})),_e(_t,{appName:"trace",htmlFile:"trace-app.html",toolName:"atoms_trace",title:"Trace Item Relationships",description:'Walk the traceability graph from a starting item.\n\nAnswers questions like "which requirements does TC-00003 verify?" or\n"if I change REQ-00001, what\'s affected downstream?"\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive graph visualization.\nFalls back to flat list for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project containing the item\n - item_id (string): Starting item ID\n - direction (string): upstream|downstream|both\n - upstream: follow parent/verifies links (dependencies)\n - downstream: follow child/verified_by links (dependents)\n - both: trace in both directions\n - depth (number, optional): Max traversal depth (default 5, max 10)\n - relationship_types (string[], optional): Filter to specific types (parent, child, related, verifies, verified_by)\n\nReturns:\n { root, direction, items: [{ id, title, type, relationship, depth }], total_count }\n\nExamples:\n - "What does TC-00003 verify?" → atoms_trace(project_id, "TC-00003", "upstream")\n - "What\'s affected by REQ-00001?" → atoms_trace(project_id, "REQ-00001", "downstream")',inputSchema:{project_id:_.string().uuid().describe("UUID of the project"),item_id:_.string().min(1).max(20).describe("Starting item ID"),direction:_.enum(["upstream","downstream","both"]).describe("Traversal direction"),depth:_.number().int().min(1).max(10).default(5).describe("Max traversal depth (default 5)"),relationship_types:_.array(_.enum(["parent","child","related","verifies","verified_by"])).optional().describe("Filter to specific relationship types")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:wt("atoms_trace",async e=>{const{traceHandler:t}=await Promise.resolve().then(()=>(pt(),lt));return t(e)})}),_e(_t,{appName:"summary",htmlFile:"summary-app.html",toolName:"atoms_project_summary",title:"Project Compliance Summary",description:'One-call project health and compliance dashboard.\n\nReturns item counts by type, test execution status, requirement coverage\n(overall and by domain), and recent change activity.\n\nIn MCP App hosts (Claude, ChatGPT), renders an interactive dashboard with charts.\nFalls back to JSON for non-UI clients.\n\nArgs:\n - project_id (string, UUID): Project to summarize\n\nReturns:\n { project_name, counts, test_status, coverage, coverage_by_domain, recent_changes, last_updated }\n\nExamples:\n - "Give me a compliance snapshot" → atoms_project_summary(project_id)\n - "How\'s our test coverage?" → atoms_project_summary(project_id)',inputSchema:{project_id:_.string().uuid().describe("UUID of the project")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},handler:wt("atoms_project_summary",async e=>{const{projectSummaryHandler:t}=await Promise.resolve().then(()=>(yt(),ut));return t(e)})})}}),St={};async function kt(){const e=f(32).toString("base64url"),t=y("sha256").update(e).digest("base64url"),n=`http://localhost:${vt}${jt}`,r=new p(`${W}/auth/mcp-consent`);return r.searchParams.set("redirect_uri",n),r.searchParams.set("code_challenge",t),r.searchParams.set("code_challenge_method","S256"),new Promise((t,n)=>{let i;function a(){clearTimeout(i)}const s=w(async(r,i)=>{const o=new p(r.url??"/",`http://localhost:${vt}`);if(o.pathname!==jt)return i.writeHead(404),void i.end("Not found");try{if("access_denied"===o.searchParams.get("error"))return i.writeHead(200,{"Content-Type":"text/html"}),i.end(Et("Authorization was cancelled.")),a(),s.close(),void n(new Error("Authorization cancelled by user."));const r=o.searchParams.get("code");if(r){const o=await fetch(`${N}/auth/v1/token?grant_type=pkce`,{method:"POST",headers:{"Content-Type":"application/json",apikey:$},body:JSON.stringify({auth_code:r,code_verifier:e})}),c=await o.json();if(!o.ok||!c.access_token){const e=c.error_description??c.msg??"Unknown error";return i.writeHead(200,{"Content-Type":"text/html"}),i.end(Et(`Code exchange failed: ${e}`)),a(),s.close(),void n(new Error(`Code exchange failed: ${e}`))}let d="authenticated";try{d=JSON.parse(Buffer.from(c.access_token.split(".")[1],"base64").toString()).email??d}catch{}const{createClient:l}=await import("@supabase/supabase-js"),m=l(N,$,{global:{headers:{Authorization:`Bearer ${c.access_token}`}}}),{data:p,error:u}=await m.from("org_members").select("org_id").limit(1);return u||!p||0===p.length?(i.writeHead(200,{"Content-Type":"text/html"}),i.end(Et('No ATOMS account found for this Google account.<br><br>Please sign up at <a href="https://x.atoms.tech">x.atoms.tech</a> first, then run this login command again.')),a(),s.close(),void n(new Error("No ATOMS account found. Sign up at x.atoms.tech first."))):(await P({access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Date.now()+1e3*(c.expires_in??3600),user_email:d}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(It(d)),a(),s.close(),void t({email:d}))}const c=o.searchParams.get("access_token"),d=o.searchParams.get("refresh_token"),l=parseInt(o.searchParams.get("expires_in")??"3600",10);c&&d?(await P({access_token:c,refresh_token:d,expires_at:Date.now()+1e3*l}),i.writeHead(200,{"Content-Type":"text/html"}),i.end(It("authenticated")),a(),s.close(),t({email:"authenticated"})):(i.writeHead(200,{"Content-Type":"text/html"}),i.end("<!DOCTYPE html>\n<html>\n<head><title>ATOMS MCP Login</title></head>\n<body>\n <h1>Completing login...</h1>\n <script>\n const hash = window.location.hash.substring(1);\n const params = new URLSearchParams(hash);\n const data = {\n access_token: params.get('access_token'),\n refresh_token: params.get('refresh_token'),\n expires_in: params.get('expires_in'),\n user_email: ''\n };\n if (data.access_token) {\n // Decode JWT to get email\n try {\n const payload = JSON.parse(atob(data.access_token.split('.')[1]));\n data.user_email = payload.email || '';\n } catch(e) {}\n fetch('/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data)\n }).then(() => {\n document.body.innerHTML = '<h1>Login successful! You can close this tab.</h1>';\n }).catch(() => {\n document.body.innerHTML = '<h1>Login failed. Please try again.</h1>';\n });\n } else {\n document.body.innerHTML = '<h1>Login failed — no tokens received.</h1>';\n }\n <\/script>\n</body>\n</html>"))}catch(e){i.writeHead(500),i.end("Authentication failed"),a(),s.close(),n(e)}});s.on("request",async(e,r)=>{if("POST"===e.method&&"/token"===e.url){let i="";e.on("data",e=>{i+=e.toString()}),e.on("end",async()=>{try{const e=JSON.parse(i);await P({access_token:e.access_token,refresh_token:e.refresh_token,expires_at:Date.now()+1e3*(e.expires_in??3600),user_email:e.user_email}),r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0})),a(),s.close(),t({email:e.user_email??"authenticated"})}catch(e){r.writeHead(500),r.end("Failed to store credentials"),a(),s.close(),n(e)}})}}),s.listen(vt,"127.0.0.1",async()=>{process.stderr.write("\nOpening browser for ATOMS login...\n"),process.stderr.write(`If the browser doesn't open, visit:\n${r.toString()}\n\n`);try{const{default:e}=await import("open");await e(r.toString())}catch{process.stderr.write("Could not open browser automatically. Please open the URL above.\n")}}),i=setTimeout(()=>{s.close(),n(new Error("Login timed out after 60 seconds. Run 'npx @atoms-tech/atoms-mcp login' to try again."))},6e4)})}function It(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Authorized</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #052e16;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 {\n font-size: 22px;\n font-weight: 600;\n margin-bottom: 8px;\n color: #22c55e;\n }\n .email {\n font-size: 14px;\n color: #a3a3a3;\n margin-bottom: 32px;\n }\n .permissions {\n text-align: left;\n background: #0a0a0a;\n border: 1px solid #262626;\n border-radius: 12px;\n padding: 20px;\n margin-bottom: 24px;\n }\n .permissions h3 {\n font-size: 12px;\n font-weight: 600;\n color: #737373;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 12px;\n }\n .perm-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 0;\n font-size: 14px;\n color: #d4d4d4;\n }\n .perm-item .dot {\n width: 6px;\n height: 6px;\n background: #22c55e;\n border-radius: 50%;\n flex-shrink: 0;\n }\n .closing {\n font-size: 13px;\n color: #525252;\n }\n .closing span { color: #a3a3a3; font-variant-numeric: tabular-nums; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>\n </svg>\n </div>\n <h1>Authorization Granted</h1>\n <p class="email">${e}</p>\n\n <div class="permissions">\n <h3>ATOMS MCP can now</h3>\n <div class="perm-item"><span class="dot"></span> Read your projects and requirements</div>\n <div class="perm-item"><span class="dot"></span> Create and edit requirements and test cases</div>\n <div class="perm-item"><span class="dot"></span> Record test results (pass/fail)</div>\n <div class="perm-item"><span class="dot"></span> Manage traceability relationships</div>\n <div class="perm-item"><span class="dot"></span> Export coverage reports and diagrams</div>\n </div>\n\n <p class="closing">This tab will close in <span id="countdown">5</span>s</p>\n </div>\n\n <script>\n let seconds = 5;\n const el = document.getElementById('countdown');\n const timer = setInterval(() => {\n seconds--;\n el.textContent = seconds;\n if (seconds <= 0) {\n clearInterval(timer);\n window.close();\n // If window.close() is blocked (not opened by script), update message\n setTimeout(() => {\n document.querySelector('.closing').textContent = 'You can close this tab now.';\n }, 500);\n }\n }, 1000);\n <\/script>\n</body>\n</html>`}function Et(e){return`<!DOCTYPE html>\n<html>\n<head>\n <title>ATOMS MCP — Failed</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n background: #0a0a0a;\n color: #fafafa;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n background: #171717;\n border: 1px solid #262626;\n border-radius: 16px;\n padding: 48px;\n max-width: 480px;\n width: 100%;\n text-align: center;\n }\n .icon {\n width: 56px;\n height: 56px;\n background: #450a0a;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 24px;\n }\n .icon svg { width: 28px; height: 28px; }\n h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #ef4444; }\n .reason { font-size: 14px; color: #a3a3a3; line-height: 1.6; }\n .reason a { color: #7c3aed; text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class="card">\n <div class="icon">\n <svg fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2.5">\n <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class="reason">${e}</p>\n </div>\n</body>\n</html>`}C(St,{login:()=>kt});var Tt=T({"src/auth/login.ts"(){F(),L(),vt=19275,jt="/callback"}});xt();var Ct=process.argv.slice(2)[0];(async function(){switch(Ct){case"login":{const{login:e}=await Promise.resolve().then(()=>(Tt(),St));try{const{email:t}=await e();process.stderr.write(`\n ✓ Authenticated as ${t}\n`),process.stderr.write(" ✓ Credentials saved to ~/.atoms/credentials.json\n\n"),process.stderr.write(" Next steps:\n"),process.stderr.write(" $ claude mcp add atoms -- npx -y @atoms-tech/atoms-mcp\n"),process.stderr.write("\n Or add to Claude Desktop (Settings → MCP → Edit Config):\n"),process.stderr.write(' {\n "mcpServers": {\n "atoms": {\n "command": "npx",\n "args": ["-y", "@atoms-tech/atoms-mcp"]\n }\n }\n }\n\n')}catch(e){process.stderr.write(`\n ✗ ${e instanceof Error?e.message:String(e)}\n\n`),process.exit(1)}break}case"whoami":{const{getValidToken:e}=await Promise.resolve().then(()=>(K(),z));try{const{email:t,user_id:n}=await e();process.stderr.write(`\n ✓ Logged in as ${t}\n User ID: ${n}\n\n`)}catch(e){process.stderr.write("\n ✗ Not authenticated.\n Run: npx @atoms-tech/atoms-mcp login\n\n"),process.exit(1)}break}case"logout":{const{clearCredentials:e}=await Promise.resolve().then(()=>(F(),D));await e(),process.stderr.write("Logged out. Credentials removed.\n"),process.stderr.write("Run 'npx @atoms-tech/atoms-mcp login' to authenticate again.\n");break}case"--version":case"-v":process.stderr.write("@atoms-tech/atoms-mcp v0.1.0\n");break;case"--help":case"-h":process.stderr.write("ATOMS MCP Server — AI agent integration for requirements management\n\nUsage:\n npx @atoms-tech/atoms-mcp Start MCP server (stdio)\n npx @atoms-tech/atoms-mcp login Authenticate with ATOMS.tech\n npx @atoms-tech/atoms-mcp logout Switch account / clear credentials\n npx @atoms-tech/atoms-mcp whoami Show current user\n npx @atoms-tech/atoms-mcp --version Show version\n");break;default:{process.stderr.write("[atoms-mcp] Starting MCP server via stdio...\n");try{const{getValidToken:e}=await Promise.resolve().then(()=>(K(),z)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch{process.stderr.write("[atoms-mcp] No credentials found. Starting login flow...\n");try{const{login:e}=await Promise.resolve().then(()=>(Tt(),St)),{email:t}=await e();process.stderr.write(`[atoms-mcp] Authenticated as ${t}\n`)}catch(e){process.stderr.write(`[atoms-mcp] Login failed: ${e instanceof Error?e.message:String(e)}\n[atoms-mcp] Starting server without auth. Tools will fail until you run: npx @atoms-tech/atoms-mcp login\n`)}}const e=new b;await _t.connect(e),process.stderr.write("[atoms-mcp] MCP server running. Waiting for tool calls...\n")}}})().catch(e=>{process.stderr.write(`[atoms-mcp] Fatal: ${e instanceof Error?e.message:String(e)}\n`),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atoms-tech/atoms-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "MCP server for ATOMS.tech — AI agent integration for requirements management",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",