aem-mcp-server
Version:
AEM MCP server. Chat with your AEM instance for content, component, and asset operations.
2 lines (1 loc) • 5.22 kB
JavaScript
;import{getAccessToken as p}from"./aem.auth.js";import{LOGGER as l}from"../utils/logger.js";export class AEMFetch{fetch;config;token;tokenExpiry;constructor(n){this.config=n,this.fetch=null,this.token="",this.tokenExpiry=0}async init(){this.token=await this.getAuthToken(this.config.auth),this.fetch=this.getFetchInstance()}getFetchInstance(){return(n,e={})=>{const t=e.headers instanceof Headers?new Headers(e.headers):new Headers(e.headers||{});this.config.auth.clientId&&!this.config.auth.username?t.set("Authorization",`Bearer ${this.token}`):t.set("Authorization",`Basic ${this.token}`),t.has("Accept")||t.set("Accept","application/json"),t.has("Content-Type")||t.set("Content-Type","application/json");const{headers:i,...r}=e;return fetch(n,{...r,headers:t})}}async getAuthToken(n){if(n.clientId&&n.clientSecret){const e=Date.now();if(this.token&&e<this.tokenExpiry)return this.token;const t=await p(n.clientId,n.clientSecret,n.scope);return this.token=t.access_token,this.tokenExpiry=e+(t.expires_in-60)*1e3,this.token}if(n.username&&n.password)return Buffer.from(`${n.username}:${n.password}`).toString("base64");throw new Error("No authentication credentials provided")}async refreshAuthToken(){this.token="",this.tokenExpiry=0,this.token=await this.getAuthToken(this.config.auth)}getTimeoutOptions(n){let e,t,a;const i=n||this.config.timeout;return i&&(e=new AbortController,a=e.signal,t=setTimeout(()=>e.abort(),i)),{signal:a,timeoutId:t}}buildUrlWithParams(n,e){const t=this.config.host.endsWith("/")?this.config.host.slice(0,-1):this.config.host,a=n.startsWith("/")?n:`/${n}`,i=`${t}${a}`;if(!e||Object.keys(e).length===0)return i;const r=new URL(i);return Object.entries(e).forEach(([s,c])=>{c!=null&&r.searchParams.append(s,String(c))}),r.toString()}async request(n,e={},t,a){if(!this.fetch)throw new Error("AEMFetch not initialized. Call await init(config) before making requests.");const{timeoutId:i,signal:r}=this.getTimeoutOptions(t);t&&(e.signal=r),e.redirect=e.redirect||"follow";let s;try{if(s=await this.fetch(n,e),s.status===401&&(l.warn(`AEM request to ${n} returned 401 Unauthorized. Attempting to refresh token...`),await this.refreshAuthToken(),s=await this.fetch(n,e)),s.status>=300&&s.status<400&&!s.ok){const o=s.headers.get("Location");if(o){l.warn(`Redirect detected (${s.status}) from ${n} to ${o}`);const h=o.startsWith("http")?o:`${this.config.host}${o}`;s=await this.fetch(h,{...e,redirect:"follow"})}}if(!s.ok){const o=s.clone();let h=`AEM ${e.method||"GET"} failed: ${s.status}`,u=null;try{if(u=await o.text(),u&&u.trim().length>0)try{const f=JSON.parse(u);h=`AEM ${e.method||"GET"} failed: ${s.status} - ${JSON.stringify(f)}`}catch{h=`AEM ${e.method||"GET"} failed: ${s.status} - ${u}`}}catch{}const d=new Error(h);throw d.status=s.status,d.response={status:s.status,data:u||null},d}if(s.status===204||s.status===200){const o=s.headers.get("content-type")||"",h=s.headers.get("content-length");if(e.method==="DELETE"&&(!h||h==="0"))return{};if(!o.includes("application/json")&&(!h||h==="0"))return{}}if(a)return s.text();const c=await s.text();if(!c||c.trim().length===0)return{};try{return JSON.parse(c)}catch(o){if(e.method==="DELETE"&&s.status>=200&&s.status<300)return l.warn(`DELETE response was not JSON, but status ${s.status} indicates success`),{success:!0,status:s.status};throw new Error(`Failed to parse response as JSON: ${o.message}. Response: ${c.substring(0,200)}`)}}finally{i&&clearTimeout(i)}}async get(n,e,t={},a,i){const r=this.buildUrlWithParams(n,e);return this.request(r,t,a,i)}async post(n,e,t={},a,i){let r;const s=t.headers instanceof Headers?new Headers(t.headers):new Headers(t.headers||{});e instanceof URLSearchParams?(r=e,s.set("Content-Type","application/x-www-form-urlencoded")):(r=JSON.stringify(e),s.has("Content-Type")||s.set("Content-Type","application/json"));const c=this.buildUrlWithParams(n),{headers:o,...h}=t;return this.request(c,{...h,method:"POST",body:r,headers:s},a,i)}async delete(n,e={},t){const a=this.buildUrlWithParams(n);return this.request(a,{...e,method:"DELETE"},t)}async put(n,e,t={},a){let i;const r=t.headers instanceof Headers?new Headers(t.headers):new Headers(t.headers||{});e instanceof URLSearchParams?(i=e,r.set("Content-Type","application/x-www-form-urlencoded")):(i=JSON.stringify(e),r.has("Content-Type")||r.set("Content-Type","application/json"));const s=this.buildUrlWithParams(n),{headers:c,...o}=t;return this.request(s,{...o,method:"PUT",body:i,headers:r},a)}async postWithHeaders(n,e,t={},a){if(!this.fetch)throw new Error("AEMFetch not initialized. Call await init() before making requests.");let i;const r=t.headers instanceof Headers?new Headers(t.headers):new Headers(t.headers||{});e instanceof URLSearchParams?(i=e,r.set("Content-Type","application/x-www-form-urlencoded")):(i=JSON.stringify(e),r.has("Content-Type")||r.set("Content-Type","application/json"));const s=this.buildUrlWithParams(n),{timeoutId:c,signal:o}=this.getTimeoutOptions(a);a&&(t.signal=o);try{const h=await this.fetch(s,{...t,method:"POST",body:i,headers:r});return h.status===401?(await this.refreshAuthToken(),await this.fetch(s,{...t,method:"POST",body:i,headers:new Headers(r)})):h}finally{c&&clearTimeout(c)}}}