@xtrn/cli 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +15 -15
- package/package.json +1 -1
- package/templates/README.md.template +80 -7
- package/templates/package.json.template +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as V}from"node:child_process";import*as R from"node:fs";import*as _ from"node:path";import{spawn as Y}from"node:child_process";import*as x from"node:fs";import*as
|
|
3
|
-
export { default } from "${
|
|
4
|
-
`;x.writeFileSync(
|
|
2
|
+
import{spawn as V}from"node:child_process";import*as R from"node:fs";import*as _ from"node:path";import{spawn as Y}from"node:child_process";import*as x from"node:fs";import*as T from"node:os";import*as g from"node:path";function X(s){let n=s;while(!0){let o=g.join(n,"node_modules");if(x.existsSync(o))return o;let t=g.dirname(n);if(t===n)return null;n=t}}function A(s,n,o){let t=g.dirname(s),r=X(t);if(!r)console.warn("[xtrn] Warning: node_modules not found for symlink");let i=g.join(o,"node_modules");if(r&&!x.existsSync(i))x.symlinkSync(r,i,"junction");let f=`export { XTRNState } from "@xtrn/server";
|
|
3
|
+
export { default } from "${s}";
|
|
4
|
+
`;x.writeFileSync(g.join(o,"_cf_entry.ts"),f);let u=`name = "xtrn-dev"
|
|
5
5
|
main = "_cf_entry.ts"
|
|
6
6
|
compatibility_date = "2026-02-14"
|
|
7
7
|
compatibility_flags = ["nodejs_compat"]
|
|
8
8
|
|
|
9
9
|
[vars]
|
|
10
|
-
${Object.entries(n).map(([
|
|
10
|
+
${Object.entries(n).map(([a,l])=>`${a} = "${l}"`).join(`
|
|
11
11
|
`)}
|
|
12
12
|
|
|
13
13
|
[[durable_objects.bindings]]
|
|
@@ -17,9 +17,9 @@ class_name = "XTRNState"
|
|
|
17
17
|
[[migrations]]
|
|
18
18
|
tag = "v1"
|
|
19
19
|
new_sqlite_classes = ["XTRNState"]
|
|
20
|
-
`;x.writeFileSync(
|
|
20
|
+
`;x.writeFileSync(g.join(o,"wrangler.toml"),u)}async function O(s,n){let o=g.dirname(s),t=g.basename(s),r=g.join(n,t);x.copyFileSync(s,r);let i=X(o);if(!i)console.warn("[xtrn] Warning: node_modules not found for symlink");let f=g.join(n,"node_modules");if(i&&!x.existsSync(f))x.symlinkSync(i,f,"junction");let u=`export { XTRNState } from "@xtrn/server";
|
|
21
21
|
export { default } from "${`./${t}`}";
|
|
22
|
-
`,
|
|
22
|
+
`,a=g.join(n,"_cf_entry.ts");x.writeFileSync(a,u);let l=`name = "xtrn-dev"
|
|
23
23
|
main = "_cf_entry.ts"
|
|
24
24
|
compatibility_date = "2026-02-14"
|
|
25
25
|
compatibility_flags = ["nodejs_compat"]
|
|
@@ -31,15 +31,15 @@ class_name = "XTRNState"
|
|
|
31
31
|
[[migrations]]
|
|
32
32
|
tag = "v1"
|
|
33
33
|
new_sqlite_classes = ["XTRNState"]
|
|
34
|
-
`;return x.writeFileSync(
|
|
35
|
-
${
|
|
36
|
-
`)){let
|
|
37
|
-
${
|
|
38
|
-
`}await
|
|
39
|
-
`}async function vn(
|
|
34
|
+
`;return x.writeFileSync(g.join(n,"wrangler.toml"),l),await Z(n),g.join(n,"_cf_entry.js")}function Z(s){return new Promise((n,o)=>{let t=Y("wrangler",["deploy","--dry-run","--outdir","."],{cwd:s,stdio:["ignore","pipe","pipe"]}),r="";t.stdout?.on("data",()=>{}),t.stderr?.on("data",(i)=>{r+=i.toString()}),t.on("error",(i)=>{o(Error(`Failed to spawn wrangler: ${i.message}`))}),t.on("close",(i)=>{if(i!==0){o(Error(`wrangler deploy --dry-run failed (exit ${i}):
|
|
35
|
+
${r}`));return}n()})})}function d(){return x.mkdtempSync(g.join(T.tmpdir(),"xtrn-dev-"))}function S(s){x.rmSync(s,{recursive:!0,force:!0})}import*as $ from"node:fs";import*as F from"node:path";import{Miniflare as G}from"miniflare";function K(s,n,o,t){let r=t||F.dirname(s);return{modules:!0,scriptPath:s,modulesRoot:r,compatibilityDate:"2026-02-14",compatibilityFlags:["nodejs_compat"],durableObjects:{XTRN_STATE:"XTRNState"},durableObjectsPersist:!0,port:n,bindings:o}}async function C(s,n,o,t){let r=K(s,n,o,t),i=new G(r);return await i.ready,i}function q(s){if(!$.existsSync(s))return{};let n=$.readFileSync(s,"utf-8"),o={};for(let t of n.split(`
|
|
36
|
+
`)){let r=t.trim();if(!r||r.startsWith("#"))continue;let i=r.indexOf("=");if(i===-1)continue;o[r.slice(0,i)]=r.slice(i+1)}return o}async function W(s){return await(await s.dispatchFetch("http://localhost/details")).json()}var e={reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",magenta:"\x1B[35m",white:"\x1B[37m",red:"\x1B[31m",gray:"\x1B[90m"};function P(s){let n="./index.ts",o=1234;for(let t of s)if(t.startsWith("--port="))o=Number.parseInt(t.slice(t.indexOf("=")+1),10);else if(!t.startsWith("--"))n=t;return{entryPoint:_.resolve(n),port:o}}async function k(s){for(let o=0;o<5;o++){try{let t=await fetch(`http://localhost:${s}/details`);if(t.ok)return await t.json()}catch{}await new Promise((t)=>setTimeout(t,200))}throw Error("Failed to fetch /details after server ready")}function D(s,n){let o=`http://localhost:${s}`;if(console.log(""),console.log(` ${e.bold}${e.cyan}${n.name}${e.reset} ${e.dim}${n.version}${e.reset}`),console.log(` ${e.green}➜${e.reset} ${e.bold}${o}${e.reset}`),console.log(""),n.tools.length>0){console.log(` ${e.bold}${e.white}Tools${e.reset}`);for(let t of n.tools){let r=t.tags.length>0?` ${e.dim}${t.tags.map((i)=>`[${i}]`).join(" ")}${e.reset}`:"";console.log(` ${e.cyan}${t.name}${e.reset}${r} ${e.gray}— ${t.description}${e.reset}`)}console.log("")}if(n.config.length>0){console.log(` ${e.bold}${e.white}Config${e.reset}`);for(let t of n.config)console.log(` ${e.yellow}${t.key}${e.reset} ${e.dim}${t.type}${e.reset}`);console.log("")}if(n.requiredEnv.length>0){console.log(` ${e.bold}${e.white}Required Env${e.reset}`);for(let t of n.requiredEnv)console.log(` ${e.yellow}${t}${e.reset}`);console.log("")}if(n.oauth)console.log(` ${e.bold}${e.white}OAuth${e.reset} ${e.magenta}${n.oauth.provider}${e.reset}`),console.log(` ${e.dim}authorize${e.reset} ${n.oauth.authorization_url}`),console.log(` ${e.dim}token${e.reset} ${n.oauth.token_url}`),console.log(` ${e.dim}callback${e.reset} ${n.oauth.callback_url}`),console.log(` ${e.dim}scopes${e.reset} ${n.oauth.scopes.join(", ")}`),console.log("")}function nn(s,n){let o=_.join(s,"wrangler.toml"),t=V("npx",["wrangler","dev","--config",o,"--port",String(n)],{cwd:s,stdio:["ignore","pipe","pipe"]}),r="",i=new Promise((f,m)=>{let u=(l)=>{if(l.toString().includes("Ready on"))t.stdout?.off("data",u),f()},a=(l)=>{r+=l.toString()};t.stdout?.on("data",u),t.stderr?.on("data",a),t.on("error",(l)=>m(l)),t.on("close",(l)=>{if(l!==0&&l!==null)m(Error(r.trim()||`wrangler exited with code ${l}`))})});return i.then(()=>{r="",t.stderr?.on("data",(f)=>{let m=f.toString();if(m.includes("ERROR")||m.includes("Error")||m.includes("error:"))process.stderr.write(`${e.red}[xtrn dev]${e.reset} ${m}`)})}),{proc:t,ready:i}}async function I(s){let{entryPoint:n,port:o}=P(s),t=_.dirname(n);if(!R.existsSync(n))console.error(`${e.red}[xtrn dev]${e.reset} Entry point not found: ${n}`),process.exit(1);if(!R.existsSync(_.join(t,"package.json")))console.error(`${e.red}[xtrn dev]${e.reset} No package.json found in ${t}`),process.exit(1);let r=_.join(t,".dev.vars"),i=q(r),f=d();A(n,i,f);let m=null,u=()=>{if(console.log(`
|
|
37
|
+
${e.dim}[xtrn dev] Shutting down...${e.reset}`),m)m.kill(),m=null;S(f),process.exit(0)};process.on("SIGINT",u),process.on("SIGTERM",u);try{console.log(`${e.dim}[xtrn dev] Starting...${e.reset}`);let a=nn(f,o);m=a.proc,m.on("error",(c)=>{console.error(`${e.red}[xtrn dev]${e.reset} Failed to start: ${c.message}`),S(f),process.exit(1)}),m.on("close",(c)=>{if(c!==0&&c!==null)console.error(`${e.red}[xtrn dev]${e.reset} Process exited with code ${c}`);S(f),process.exit(c??1)}),await a.ready;let l=await k(o);D(o,l),await new Promise(()=>{})}catch(a){if(m)m.kill();if(S(f),a instanceof Error)console.error(`${e.red}[xtrn dev]${e.reset} ${a.message}`);process.exit(1)}}import{mkdir as U,readFile as tn,writeFile as on}from"node:fs/promises";import{dirname as sn,join as N}from"node:path";import{fileURLToPath as en}from"node:url";async function H(s){let n=s[0],o=s[1]??"v1.0.0";if(!n)console.error("Error: <name> argument is required"),console.error("Usage: xtrn new <name> [version]"),process.exit(1);let t=o.startsWith("v")?o:`v${o}`,r=t.replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(r))console.error(`Error: Invalid version "${o}". Expected semver (e.g., v1.0.0)`),process.exit(1);let i=en(import.meta.url),f=sn(i),m=N(f,"../templates"),u=N(process.cwd(),n);try{await U(u,{recursive:!1})}catch(a){if(a instanceof Error&&"code"in a&&a.code==="EEXIST")console.error(`Error: Directory "${n}" already exists`),process.exit(1);throw a}try{let a=[{src:"index.ts.template",dst:"index.ts"},{src:"package.json.template",dst:"package.json"},{src:"tsconfig.json.template",dst:"tsconfig.json"},{src:"README.md.template",dst:"README.md"}];for(let l of a){let c=N(m,l.src),j=N(u,l.dst),w=await tn(c,"utf-8");if(w=w.replace(/\{\{NAME\}\}/g,n),w=w.replace(/\{\{VERSION\}\}/g,t),w=w.replace(/\{\{VERSION_BARE\}\}/g,r),l.dst==="package.json"){let E=JSON.parse(w);E.dependencies["@xtrn/server"]="^1.0.0",E.devDependencies["@xtrn/cli"]="^1.0.0",E.scripts.submit="xtrn submit",w=`${JSON.stringify(E,null,"\t")}
|
|
38
|
+
`}await on(j,w,"utf-8")}console.log(`✓ Created ${n}/`),console.log(""),console.log("Next steps:"),console.log(` cd ${n}`),console.log(" bun install"),console.log(" bun run dev")}catch(a){try{await U(u,{recursive:!0})}catch{}throw a}}import{spawn as rn}from"node:child_process";import*as y from"node:fs";import{cp as J,mkdir as z,readFile as fn,readdir as mn,writeFile as an}from"node:fs/promises";import*as M from"node:os";import*as v from"node:path";var p="xtrnai/servers",ln=new Set(["node_modules","bun.lock",".dev.vars","dist","_cf_entry.ts","wrangler.toml",".wrangler",".git",".github"]);function b(s,n,o){return new Promise((t)=>{let r=rn(s,n,{stdio:"pipe",cwd:o?.cwd}),i="",f="";r.stdout.on("data",(m)=>{i+=m.toString()}),r.stderr.on("data",(m)=>{f+=m.toString()}),r.on("close",(m)=>{t({stdout:i.trim(),stderr:f.trim(),code:m??1})}),r.on("error",()=>{t({stdout:"",stderr:`Failed to execute: ${s}`,code:1})})})}async function xn(){if((await b("gh",["--version"])).code!==0)console.error("[xtrn submit] GitHub CLI (gh) is not installed."),console.error(""),console.error("Install it:"),console.error(" macOS: brew install gh"),console.error(" Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md"),console.error(" Windows: winget install --id GitHub.cli"),console.error(""),console.error("Then authenticate:"),console.error(" gh auth login"),process.exit(1);if((await b("gh",["auth","status"])).code!==0)console.error("[xtrn submit] GitHub CLI is not authenticated."),console.error(""),console.error("Run:"),console.error(" gh auth login"),process.exit(1)}function gn(s){if(!y.existsSync(v.join(s,"index.ts")))console.error("[xtrn submit] No index.ts found in current directory."),console.error("Run this command from your XTRN server project root."),process.exit(1);if(!y.existsSync(v.join(s,"package.json")))console.error("[xtrn submit] No package.json found in current directory."),console.error("Run this command from your XTRN server project root."),process.exit(1)}async function un(s){let n=v.resolve(s,"index.ts"),o=d(),t=null;try{console.log("[xtrn submit] Bundling server...");let r=await O(n,o);console.log("[xtrn submit] Starting headless Miniflare..."),t=await C(r,0,{});let i=await W(t),f=i.name,m=i.version;if(typeof f!=="string"||!f)throw Error("Server /details missing 'name' field");if(typeof m!=="string"||!m)throw Error("Server /details missing 'version' field");return console.log(`[xtrn submit] Detected: ${f} v${m}`),{name:f,version:m}}finally{if(t)await t.dispose();S(o)}}async function B(s,n){await z(n,{recursive:!0});let o=await mn(s,{withFileTypes:!0});for(let t of o){if(ln.has(t.name))continue;let r=v.join(s,t.name),i=v.join(n,t.name);if(t.isDirectory())await B(r,i);else await J(r,i)}}function cn(s){let n=JSON.parse(s),o=(t)=>{if(!t||typeof t!=="object")return;let r={};for(let[i,f]of Object.entries(t))if(typeof f==="string"&&(f.startsWith("file:")||f.startsWith("link:")||f.startsWith("workspace:")))r[i]="^1.0.0";else r[i]=f;return r};if(n.dependencies)n.dependencies=o(n.dependencies);if(n.devDependencies)n.devDependencies=o(n.devDependencies);return`${JSON.stringify(n,null,"\t")}
|
|
39
|
+
`}async function vn(s,n){console.log("[xtrn submit] Preparing files..."),await B(s,n);let o=v.join(n,"package.json");if(y.existsSync(o)){let t=await fn(o,"utf-8"),r=cn(t);await an(o,r,"utf-8"),console.log("[xtrn submit] Rewrote package.json (local deps → ^1.0.0)")}}async function yn(s,n,o){let t=y.mkdtempSync(v.join(M.tmpdir(),"xtrn-submit-")),r=v.join(t,"xtrn-servers");try{console.log(`[xtrn submit] Forking ${p}...`);let i=await b("gh",["repo","fork",p,"--clone=true","--default-branch-only"],{cwd:t});if(i.code!==0&&!i.stderr.includes("already exists"))throw Error(`Fork failed: ${i.stderr}`);if(!y.existsSync(r))throw Error(`Expected cloned repo at ${r}, not found. Output: ${i.stdout} ${i.stderr}`);let f=`submit/${s}/v${n}`;console.log(`[xtrn submit] Creating branch ${f}...`);let m=await b("git",["checkout","-b",f],{cwd:r});if(m.code!==0)throw Error(`Branch creation failed: ${m.stderr}`);let u=v.join(r,"servers",s,`v${n}`);await z(u,{recursive:!0}),await J(o,u,{recursive:!0}),console.log("[xtrn submit] Committing...");let a=await b("git",["add","."],{cwd:r});if(a.code!==0)throw Error(`git add failed: ${a.stderr}`);let l=await b("git",["commit","-m",`Add ${s} v${n}`],{cwd:r});if(l.code!==0)throw Error(`git commit failed: ${l.stderr}`);console.log("[xtrn submit] Pushing...");let c=await b("git",["push","origin",f],{cwd:r});if(c.code!==0)throw Error(`git push failed: ${c.stderr}`);console.log("[xtrn submit] Creating pull request...");let j=await b("gh",["pr","create","--repo",p,"--title",`Add ${s} v${n}`,"--body",`Automated submission via \`xtrn submit\` CLI.
|
|
40
40
|
|
|
41
|
-
**Server:** ${
|
|
42
|
-
**Version:** v${n}`],{cwd:
|
|
41
|
+
**Server:** ${s}
|
|
42
|
+
**Version:** v${n}`],{cwd:r});if(j.code!==0)throw Error(`PR creation failed: ${j.stderr}`);return j.stdout}finally{y.rmSync(t,{recursive:!0,force:!0})}}async function L(s){let n=process.cwd();await xn(),gn(n);let{name:o,version:t}=await un(n),r=y.mkdtempSync(v.join(M.tmpdir(),"xtrn-prepared-"));try{await vn(n,r);let i=await yn(o,t,r);console.log(""),console.log("✓ Pull request created!"),console.log(` ${i}`),console.log(""),console.log("Your server will be reviewed and deployed automatically once merged.")}finally{y.rmSync(r,{recursive:!0,force:!0})}}var Q="1.0.0",h=`
|
|
43
43
|
xtrn v${Q} — XTRN server development toolkit
|
|
44
44
|
|
|
45
45
|
Usage:
|
|
@@ -63,4 +63,4 @@ Examples:
|
|
|
63
63
|
xtrn dev ./server.ts Start dev server with custom entry
|
|
64
64
|
xtrn dev --port=3000 Start dev server on port 3000
|
|
65
65
|
xtrn submit Fork xtrn-servers and open PR
|
|
66
|
-
`.trim();async function wn(){let
|
|
66
|
+
`.trim();async function wn(){let s=process.argv.slice(2),n=s[0];if(!n||n==="--help"||n==="-h")console.log(h),process.exit(0);if(n==="--version"||n==="-v")console.log(`xtrn v${Q}`),process.exit(0);if(n==="new"){let o=s.slice(1);if(o.includes("--help")||o.includes("-h"))console.log(h),process.exit(0);await H(o);return}if(n==="dev"){let o=s.slice(1);if(o.includes("--help")||o.includes("-h"))console.log(h),process.exit(0);await I(o);return}if(n==="submit"){let o=s.slice(1);if(o.includes("--help")||o.includes("-h"))console.log(h),process.exit(0);await L(o);return}console.error(`Unknown command: ${n}`),console.error('Run "xtrn --help" for usage.'),process.exit(1)}wn().catch((s)=>{console.error("[xtrn] Fatal:",s),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -19,10 +19,10 @@ Then open `http://localhost:1234/details`.
|
|
|
19
19
|
|
|
20
20
|
## Server API
|
|
21
21
|
|
|
22
|
-
Import everything from `@xtrn/server
|
|
22
|
+
Import everything from `@xtrn/server`:
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server
|
|
25
|
+
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server";
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -31,7 +31,7 @@ import { z } from "zod";
|
|
|
31
31
|
No configuration or authentication required.
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
import { XTRNServer, defineConfig } from "@xtrn/server
|
|
34
|
+
import { XTRNServer, defineConfig } from "@xtrn/server";
|
|
35
35
|
import { z } from "zod";
|
|
36
36
|
|
|
37
37
|
const server = new XTRNServer({
|
|
@@ -59,7 +59,7 @@ export default server;
|
|
|
59
59
|
Requires users to provide configuration values (API keys, preferences).
|
|
60
60
|
|
|
61
61
|
```typescript
|
|
62
|
-
import { XTRNServer, defineConfig } from "@xtrn/server
|
|
62
|
+
import { XTRNServer, defineConfig } from "@xtrn/server";
|
|
63
63
|
import { z } from "zod";
|
|
64
64
|
|
|
65
65
|
const server = new XTRNServer({
|
|
@@ -94,7 +94,7 @@ export default server;
|
|
|
94
94
|
Requires users to complete an OAuth flow. The platform provides a fresh access token directly.
|
|
95
95
|
|
|
96
96
|
```typescript
|
|
97
|
-
import { XTRNServer, defineConfig } from "@xtrn/server
|
|
97
|
+
import { XTRNServer, defineConfig } from "@xtrn/server";
|
|
98
98
|
import { z } from "zod";
|
|
99
99
|
|
|
100
100
|
const server = new XTRNServer({
|
|
@@ -133,7 +133,7 @@ export default server;
|
|
|
133
133
|
Combines both user configuration and OAuth authentication.
|
|
134
134
|
|
|
135
135
|
```typescript
|
|
136
|
-
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server
|
|
136
|
+
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server";
|
|
137
137
|
import { z } from "zod";
|
|
138
138
|
|
|
139
139
|
const server = new XTRNServer({
|
|
@@ -173,6 +173,39 @@ server.registerTool({
|
|
|
173
173
|
export default server;
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### Env-Only Server
|
|
177
|
+
|
|
178
|
+
Requires specific environment variables to be set by the deployer (e.g., internal API keys, base URLs).
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { XTRNServer, defineConfig } from "@xtrn/server";
|
|
182
|
+
import { z } from "zod";
|
|
183
|
+
|
|
184
|
+
const server = new XTRNServer({
|
|
185
|
+
name: "env-server",
|
|
186
|
+
version: "1.0.0",
|
|
187
|
+
config: defineConfig({
|
|
188
|
+
requiredEnv: ["API_KEY", "BASE_URL"],
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
server.registerTool({
|
|
193
|
+
name: "fetch",
|
|
194
|
+
description: "Fetch data from internal API",
|
|
195
|
+
schema: z.object({ endpoint: z.string() }),
|
|
196
|
+
handler: async (ctx) => {
|
|
197
|
+
// ctx.env.API_KEY — string (typed from requiredEnv)
|
|
198
|
+
// ctx.env.BASE_URL — string (typed from requiredEnv)
|
|
199
|
+
const response = await fetch(`${ctx.env.BASE_URL}/${ctx.req.endpoint}`, {
|
|
200
|
+
headers: { Authorization: `Bearer ${ctx.env.API_KEY}` },
|
|
201
|
+
});
|
|
202
|
+
return ctx.res.json(await response.json());
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export default server;
|
|
207
|
+
```
|
|
208
|
+
|
|
176
209
|
## OAuth Configuration
|
|
177
210
|
|
|
178
211
|
```typescript
|
|
@@ -200,6 +233,45 @@ OAUTH_CLIENT_SECRET=your-client-secret
|
|
|
200
233
|
OAUTH_CALLBACK_URL=http://localhost:1234/auth/callback
|
|
201
234
|
```
|
|
202
235
|
|
|
236
|
+
## Required Environment Variables
|
|
237
|
+
|
|
238
|
+
Server developers can declare environment variables that must be set by the deployer. Useful for internal API keys, base URLs, or other secrets that shouldn't be managed by end users.
|
|
239
|
+
|
|
240
|
+
### Declaration
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
config: defineConfig({
|
|
244
|
+
requiredEnv: ["API_KEY", "BASE_URL"],
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Access in Handlers
|
|
249
|
+
|
|
250
|
+
Environment variables are fully typed from the `requiredEnv` array:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
handler: async (ctx) => {
|
|
254
|
+
ctx.env.API_KEY // string
|
|
255
|
+
ctx.env.BASE_URL // string
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Local Development
|
|
260
|
+
|
|
261
|
+
Add to `.dev.vars` in your server directory:
|
|
262
|
+
|
|
263
|
+
```env
|
|
264
|
+
API_KEY=your-api-key
|
|
265
|
+
BASE_URL=https://api.example.com
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Validation
|
|
269
|
+
|
|
270
|
+
- `/details` returns `requiredEnv` array (no validation on this route)
|
|
271
|
+
- Tool calls validate all required env vars are present
|
|
272
|
+
- Missing vars return HTTP 500 listing which vars are missing
|
|
273
|
+
- Names starting with `OAUTH_` or `XTRN_` are reserved and rejected at construction
|
|
274
|
+
|
|
203
275
|
## Context Object
|
|
204
276
|
|
|
205
277
|
| Property | Type | Availability | Description |
|
|
@@ -207,12 +279,13 @@ OAUTH_CALLBACK_URL=http://localhost:1234/auth/callback
|
|
|
207
279
|
| `ctx.req` | `T` (inferred from schema) | Always | Validated tool parameters |
|
|
208
280
|
| `ctx.config` | `Object` (inferred from userConfig) | Always (`{}` if no userConfig) | Validated user configuration |
|
|
209
281
|
| `ctx.accessToken` | `string` | OAuth servers only | Access token from platform |
|
|
282
|
+
| `ctx.env` | `Object` (inferred from requiredEnv) | Always (`{}` if no requiredEnv) | Required environment variables |
|
|
210
283
|
| `ctx.res` | `XTRNResponse` | Always | Response helper methods |
|
|
211
284
|
|
|
212
285
|
## Tool Tags
|
|
213
286
|
|
|
214
287
|
```typescript
|
|
215
|
-
import { ToolTag } from "@xtrn/server
|
|
288
|
+
import { ToolTag } from "@xtrn/server";
|
|
216
289
|
|
|
217
290
|
server.registerTool({
|
|
218
291
|
name: "delete-item",
|