@nitrostack/cli 1.0.10 → 1.0.13

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.
Files changed (36) hide show
  1. package/README.md +38 -52
  2. package/dist/commands/build.d.ts.map +1 -1
  3. package/dist/commands/build.js +13 -1
  4. package/dist/commands/cursor.d.ts +11 -0
  5. package/dist/commands/cursor.d.ts.map +1 -0
  6. package/dist/commands/cursor.js +238 -0
  7. package/dist/commands/generate.d.ts.map +1 -1
  8. package/dist/commands/generate.js +5 -6
  9. package/dist/commands/init.js +3 -3
  10. package/dist/commands/start.d.ts.map +1 -1
  11. package/dist/commands/start.js +2 -0
  12. package/dist/commands/upgrade.d.ts +12 -0
  13. package/dist/commands/upgrade.d.ts.map +1 -1
  14. package/dist/commands/upgrade.js +144 -106
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +13 -0
  18. package/package.json +4 -4
  19. package/templates/typescript-oauth/.env.example +75 -14
  20. package/templates/typescript-oauth/README.md +37 -51
  21. package/templates/typescript-oauth/package.json +2 -1
  22. package/templates/typescript-oauth/src/app.module.ts +7 -0
  23. package/templates/typescript-oauth/src/guards/oauth.guard.ts +19 -0
  24. package/templates/typescript-oauth/src/index.ts +9 -11
  25. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +19 -1
  26. package/templates/typescript-oauth/src/services/duffel.service.ts +4 -2
  27. package/templates/typescript-oauth/src/widgets/package.json +2 -1
  28. package/templates/typescript-pizzaz/.env.example +14 -0
  29. package/templates/typescript-pizzaz/README.md +35 -60
  30. package/templates/typescript-pizzaz/package.json +2 -1
  31. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +17 -3
  32. package/templates/typescript-pizzaz/src/widgets/package.json +1 -0
  33. package/templates/typescript-starter/.env.example +11 -5
  34. package/templates/typescript-starter/README.md +28 -68
  35. package/templates/typescript-starter/package.json +2 -1
  36. package/templates/typescript-starter/src/widgets/package.json +2 -1
@@ -2,22 +2,57 @@ import chalk from 'chalk';
2
2
  import { execSync } from 'child_process';
3
3
  import path from 'path';
4
4
  import fs from 'fs-extra';
5
+ import https from 'https';
5
6
  import { createHeader, createBox, createSuccessBox, createErrorBox, NitroSpinner, log, spacer, nextSteps, NITRO_BANNER_FULL, showFooter } from '../ui/branding.js';
6
7
  import { trackEvent, shutdownAnalytics } from '../analytics/posthog.js';
7
8
  /**
8
- * Get the latest version of a package from npm registry
9
+ * Fetch a package's latest published version from NPM using standard https module.
9
10
  */
10
- function getLatestVersion(packageName = '@nitrostack/core') {
11
- try {
12
- const result = execSync(`npm view ${packageName} version`, {
13
- encoding: 'utf-8',
14
- stdio: ['pipe', 'pipe', 'pipe'],
15
- }).trim();
16
- return result;
17
- }
18
- catch {
19
- throw new Error(`Failed to fetch latest version for ${packageName} from npm`);
20
- }
11
+ export function fetchLatestNpmVersion(packageName) {
12
+ return new Promise((resolve, reject) => {
13
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
14
+ const req = https.get(url, {
15
+ headers: {
16
+ 'Accept': 'application/json',
17
+ 'User-Agent': 'nitrostack-cli-upgrade'
18
+ },
19
+ timeout: 5000
20
+ }, (res) => {
21
+ if (res.statusCode !== 200) {
22
+ reject(new Error(`Registry responded with HTTP ${res.statusCode}`));
23
+ return;
24
+ }
25
+ let data = '';
26
+ res.on('data', (chunk) => {
27
+ data += chunk;
28
+ });
29
+ // Handle stream errors
30
+ res.on('error', (err) => {
31
+ reject(new Error(`Response stream error: ${err.message}`));
32
+ });
33
+ res.on('end', () => {
34
+ try {
35
+ const parsed = JSON.parse(data);
36
+ if (parsed.version) {
37
+ resolve(parsed.version);
38
+ }
39
+ else {
40
+ reject(new Error('Invalid response structure from NPM registry'));
41
+ }
42
+ }
43
+ catch (e) {
44
+ reject(new Error(`Failed to parse response: ${e.message}`));
45
+ }
46
+ });
47
+ });
48
+ req.on('error', (err) => {
49
+ reject(err);
50
+ });
51
+ req.on('timeout', () => {
52
+ req.destroy();
53
+ reject(new Error('Request timed out'));
54
+ });
55
+ });
21
56
  }
22
57
  /**
23
58
  * Get the current installed version of @nitrostack/core from package.json
@@ -32,15 +67,19 @@ function getCoreVersion(packageJsonPath) {
32
67
  null;
33
68
  }
34
69
  /**
35
- * Parse version string to extract the actual version number
70
+ * Parse version string to extract the actual numeric version.
71
+ *
72
+ * Strips any leading range operator (^ ~ >= <= > <) and drops the pre-release
73
+ * suffix (e.g. `-beta.1`) so the dot-separated segments always parse to numbers
74
+ * instead of producing `NaN` in `compareVersions`.
36
75
  */
37
76
  function parseVersion(versionString) {
38
- return versionString.replace(/^[\^~>=<]+/, '');
77
+ return versionString.replace(/^[\^~>=<]+/, '').split('-')[0];
39
78
  }
40
79
  /**
41
80
  * Compare two version strings
42
81
  */
43
- function compareVersions(v1, v2) {
82
+ export function compareVersions(v1, v2) {
44
83
  const parts1 = parseVersion(v1).split('.').map(Number);
45
84
  const parts2 = parseVersion(v2).split('.').map(Number);
46
85
  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
@@ -54,37 +93,58 @@ function compareVersions(v1, v2) {
54
93
  return 0;
55
94
  }
56
95
  /**
57
- * Update all @nitrostack/* versions in package.json
96
+ * Determine if a dependency refers to a local file or workspace link.
58
97
  */
59
- function updatePackageJson(packageJsonPath, newVersion, dryRun) {
98
+ export function isLocalDependency(version) {
99
+ return version.startsWith('file:') ||
100
+ version.startsWith('link:') ||
101
+ version.startsWith('workspace:') ||
102
+ version.startsWith('.') ||
103
+ version.startsWith('/');
104
+ }
105
+ /**
106
+ * Update all @nitrostack/* versions in package.json using dynamic npm registry fetch
107
+ */
108
+ async function updatePackageJson(packageJsonPath, dryRun) {
60
109
  if (!fs.existsSync(packageJsonPath)) {
61
110
  return [];
62
111
  }
63
112
  const packageJson = fs.readJSONSync(packageJsonPath);
64
113
  const results = [];
65
114
  let hasChanges = false;
66
- const updateDeps = (deps) => {
115
+ const updateDeps = async (deps) => {
67
116
  if (!deps)
68
117
  return;
69
- for (const pkg of Object.keys(deps)) {
118
+ const promises = Object.keys(deps).map(async (pkg) => {
70
119
  if (pkg.startsWith('@nitrostack/')) {
71
120
  const currentVersion = deps[pkg];
72
- if (compareVersions(currentVersion, newVersion) < 0) {
73
- results.push({
74
- location: path.basename(path.dirname(packageJsonPath)),
75
- packageName: pkg,
76
- previousVersion: currentVersion,
77
- newVersion: `^${newVersion}`,
78
- upgraded: true,
79
- });
80
- deps[pkg] = `^${newVersion}`;
81
- hasChanges = true;
121
+ // Skip local dependencies entirely
122
+ if (isLocalDependency(currentVersion)) {
123
+ return;
124
+ }
125
+ try {
126
+ const latestVersion = await fetchLatestNpmVersion(pkg);
127
+ if (compareVersions(currentVersion, latestVersion) < 0) {
128
+ results.push({
129
+ location: path.basename(path.dirname(packageJsonPath)),
130
+ packageName: pkg,
131
+ previousVersion: currentVersion,
132
+ newVersion: `^${latestVersion}`,
133
+ upgraded: true,
134
+ });
135
+ deps[pkg] = `^${latestVersion}`;
136
+ hasChanges = true;
137
+ }
138
+ }
139
+ catch (error) {
140
+ console.warn(`\n⚠️ Skipped upgrade check for ${pkg}: ${error.message}`);
82
141
  }
83
142
  }
84
- }
143
+ });
144
+ await Promise.all(promises);
85
145
  };
86
- updateDeps(packageJson.dependencies);
87
- updateDeps(packageJson.devDependencies);
146
+ await updateDeps(packageJson.dependencies);
147
+ await updateDeps(packageJson.devDependencies);
88
148
  if (hasChanges && !dryRun) {
89
149
  fs.writeJSONSync(packageJsonPath, packageJson, { spaces: 2 });
90
150
  }
@@ -123,51 +183,22 @@ export async function upgradeCommand(options) {
123
183
  console.log(createErrorBox('Not a NitroStack Project', '@nitrostack/core is not a dependency'));
124
184
  process.exit(1);
125
185
  }
126
- // Fetch latest version
127
- const spinner = new NitroSpinner('Checking for updates...').start();
128
- let latestVersion;
129
- try {
130
- latestVersion = getLatestVersion('@nitrostack/core');
131
- spinner.succeed(`Latest version: ${chalk.cyan(latestVersion)}`);
132
- }
133
- catch (error) {
134
- spinner.fail('Failed to fetch latest version');
135
- process.exit(1);
136
- }
137
- // Check if already on latest
138
- const currentParsedVersion = parseVersion(coreVersion);
139
- if (compareVersions(currentParsedVersion, latestVersion) >= 0) {
140
- spacer();
141
- console.log(createSuccessBox('Already Up to Date', [
142
- `Current version: ${currentParsedVersion}`,
143
- `Latest version: ${latestVersion}`,
144
- ]));
145
- trackEvent('cli_upgrade_completed', {
146
- packages_upgraded: 0,
147
- from_version: currentParsedVersion,
148
- to_version: latestVersion,
149
- dry_run: !!options.dryRun,
150
- already_current: true,
151
- });
152
- await shutdownAnalytics();
153
- return;
154
- }
155
- const allResults = [];
156
186
  const dryRun = options.dryRun ?? false;
157
187
  if (dryRun) {
158
188
  spacer();
159
189
  log('Dry run mode - no changes will be made', 'warning');
160
190
  }
161
191
  spacer();
162
- log('Upgrading dependencies...', 'info');
192
+ log('Checking for package updates...', 'info');
163
193
  spacer();
194
+ const allResults = [];
164
195
  // Upgrade root
165
- const rootSpinner = new NitroSpinner('Updating root package.json...').start();
196
+ const rootSpinner = new NitroSpinner('Checking root package.json...').start();
166
197
  try {
167
- const results = updatePackageJson(rootPackageJsonPath, latestVersion, dryRun);
198
+ const results = await updatePackageJson(rootPackageJsonPath, dryRun);
168
199
  if (results.length > 0) {
169
200
  allResults.push(...results);
170
- rootSpinner.succeed(`Root: Updated ${results.length} @nitrostack packages`);
201
+ rootSpinner.succeed(`Root: Found ${results.length} package update(s)`);
171
202
  if (!dryRun) {
172
203
  const installSpinner = new NitroSpinner('Installing dependencies...').start();
173
204
  runNpmInstall(projectRoot);
@@ -175,7 +206,7 @@ export async function upgradeCommand(options) {
175
206
  }
176
207
  }
177
208
  else {
178
- rootSpinner.info('Root: All @nitrostack packages are up to date');
209
+ rootSpinner.info('Root: All @nitrostack packages are up to date (or local references)');
179
210
  }
180
211
  }
181
212
  catch (error) {
@@ -184,12 +215,12 @@ export async function upgradeCommand(options) {
184
215
  }
185
216
  // Upgrade widgets if they exist
186
217
  if (fs.existsSync(widgetsPackageJsonPath)) {
187
- const widgetsSpinner = new NitroSpinner('Updating widgets package.json...').start();
218
+ const widgetsSpinner = new NitroSpinner('Checking widgets package.json...').start();
188
219
  try {
189
- const results = updatePackageJson(widgetsPackageJsonPath, latestVersion, dryRun);
220
+ const results = await updatePackageJson(widgetsPackageJsonPath, dryRun);
190
221
  if (results.length > 0) {
191
222
  allResults.push(...results);
192
- widgetsSpinner.succeed(`Widgets: Updated ${results.length} @nitrostack packages`);
223
+ widgetsSpinner.succeed(`Widgets: Found ${results.length} package update(s)`);
193
224
  if (!dryRun) {
194
225
  const installSpinner = new NitroSpinner('Installing widget dependencies...').start();
195
226
  runNpmInstall(widgetsPath);
@@ -197,54 +228,61 @@ export async function upgradeCommand(options) {
197
228
  }
198
229
  }
199
230
  else {
200
- widgetsSpinner.info('Widgets: All @nitrostack packages are up to date');
231
+ widgetsSpinner.info('Widgets: All @nitrostack packages are up to date (or local references)');
201
232
  }
202
233
  }
203
234
  catch (error) {
204
235
  widgetsSpinner.fail('Failed to upgrade widgets');
236
+ console.error(error);
205
237
  }
206
238
  }
207
239
  // Summary
208
240
  spacer();
209
241
  if (allResults.length === 0) {
210
- log('No packages were upgraded', 'warning');
211
- }
212
- else {
213
- // Unique packages upgraded
214
- const uniquePackages = Array.from(new Set(allResults.map(r => r.packageName)));
215
- const summaryItems = uniquePackages.map(pkg => {
216
- const result = allResults.find(r => r.packageName === pkg);
217
- return `${pkg}: ${parseVersion(result.previousVersion)} → ${parseVersion(result.newVersion)}`;
218
- });
219
- if (dryRun) {
220
- spacer();
221
- console.log(createBox([
222
- chalk.yellow.bold('Dry run complete'),
223
- '',
224
- chalk.dim('No changes were made to your project.'),
225
- chalk.dim('Run without --dry-run to apply the upgrade.'),
226
- ], 'warning'));
227
- }
228
- else {
229
- console.log(createSuccessBox('Upgrade Complete', [
230
- ...summaryItems,
231
- '',
232
- chalk.dim(`Total updates across all packages: ${allResults.length}`)
233
- ]));
234
- nextSteps([
235
- 'Review the changes in package.json',
236
- 'Restart your development server',
237
- 'Check docs.nitrostack.ai for migration guides',
238
- ]);
239
- }
240
- showFooter();
242
+ console.log(createSuccessBox('Already Up to Date', [
243
+ 'All @nitrostack packages are already running their latest versions.',
244
+ ]));
241
245
  trackEvent('cli_upgrade_completed', {
242
- packages_upgraded: allResults.length,
243
- from_version: currentParsedVersion,
244
- to_version: latestVersion,
246
+ packages_upgraded: 0,
245
247
  dry_run: dryRun,
246
- already_current: false,
248
+ already_current: true,
247
249
  });
248
250
  await shutdownAnalytics();
251
+ return;
249
252
  }
253
+ // Unique packages upgraded
254
+ const uniquePackages = Array.from(new Set(allResults.map(r => r.packageName)));
255
+ const summaryItems = uniquePackages.map(pkg => {
256
+ const result = allResults.find(r => r.packageName === pkg);
257
+ return `${pkg}: ${parseVersion(result.previousVersion)} → ${parseVersion(result.newVersion)}`;
258
+ });
259
+ if (dryRun) {
260
+ spacer();
261
+ console.log(createBox([
262
+ chalk.yellow.bold('Dry Run - Proposed Upgrades:'),
263
+ ...summaryItems.map(item => ` • ${item}`),
264
+ '',
265
+ chalk.dim('No changes were made to your project.'),
266
+ chalk.dim('Run without --dry-run to apply the upgrade.'),
267
+ ], 'warning'));
268
+ }
269
+ else {
270
+ console.log(createSuccessBox('Upgrade Complete', [
271
+ ...summaryItems,
272
+ '',
273
+ chalk.dim(`Total updates across all packages: ${allResults.length}`)
274
+ ]));
275
+ nextSteps([
276
+ 'Review the changes in package.json',
277
+ 'Restart your development server',
278
+ 'Check docs.nitrostack.ai for migration guides',
279
+ ]);
280
+ }
281
+ showFooter();
282
+ trackEvent('cli_upgrade_completed', {
283
+ packages_upgraded: allResults.length,
284
+ dry_run: dryRun,
285
+ already_current: false,
286
+ });
287
+ await shutdownAnalytics();
250
288
  }
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export { startCommand } from './commands/start.js';
8
8
  export { generate } from './commands/generate.js';
9
9
  export { upgradeCommand } from './commands/upgrade.js';
10
10
  export { installCommand } from './commands/install.js';
11
+ export { cursorCommand } from './commands/cursor.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,aAAa,YAgE5B;AAGD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,wBAAgB,aAAa,YA4E5B;AAGD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { startCommand } from './commands/start.js';
8
8
  import { generate } from './commands/generate.js';
9
9
  import { upgradeCommand } from './commands/upgrade.js';
10
10
  import { installCommand } from './commands/install.js';
11
+ import { cursorCommand } from './commands/cursor.js';
11
12
  const require = createRequire(import.meta.url);
12
13
  const packageJson = require('../package.json');
13
14
  export function createProgram() {
@@ -64,6 +65,17 @@ export function createProgram() {
64
65
  .option('--skip-widgets', 'Skip installing widget dependencies')
65
66
  .option('--production', 'Install production dependencies only')
66
67
  .action(installCommand);
68
+ program
69
+ .command('cursor')
70
+ .alias('c')
71
+ .description('Integrate this MCP server with Cursor')
72
+ .option('-g, --global', 'Install globally to ~/.cursor/mcp.json')
73
+ .option('-l, --local', 'Install locally to .cursor/mcp.json')
74
+ .option('-t, --type <type>', 'Connection type: "command", "legacy-sse", or "streamable-http" (alias: "sse" → legacy-sse)')
75
+ .option('-u, --url <url>', 'HTTP connection URL (for legacy-sse or streamable-http)')
76
+ .option('-p, --port <port>', 'Port for default HTTP URL (for legacy-sse or streamable-http)')
77
+ .option('--force', 'Force overwrite of existing configuration')
78
+ .action(cursorCommand);
67
79
  return program;
68
80
  }
69
81
  // Re-export commands for programmatic use
@@ -74,6 +86,7 @@ export { startCommand } from './commands/start.js';
74
86
  export { generate } from './commands/generate.js';
75
87
  export { upgradeCommand } from './commands/upgrade.js';
76
88
  export { installCommand } from './commands/install.js';
89
+ export { cursorCommand } from './commands/cursor.js';
77
90
  // Run the CLI when this module is the entry point
78
91
  import { fileURLToPath } from 'url';
79
92
  import { realpathSync } from 'fs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitrostack/cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "description": "CLI for NitroStack - Create and manage MCP server projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,9 +11,9 @@
11
11
  },
12
12
  "exports": {
13
13
  ".": {
14
+ "types": "./dist/index.d.ts",
14
15
  "import": "./dist/index.js",
15
- "require": "./dist/index.js",
16
- "types": "./dist/index.d.ts"
16
+ "require": "./dist/index.js"
17
17
  }
18
18
  },
19
19
  "files": [
@@ -68,4 +68,4 @@
68
68
  "url": "https://github.com/nitrocloudofficial/nitrostack/issues"
69
69
  },
70
70
  "homepage": "https://nitrostack.ai"
71
- }
71
+ }
@@ -1,27 +1,88 @@
1
- # OAuth 2.1 MCP Server Configuration
1
+ # NitroStack Configuration
2
+ NITRO_LOG_LEVEL=info
3
+ NITROSTACK_APP_MODE=openai
4
+
5
+ # Server Transport Configuration (Optional)
2
6
  # =============================================================================
3
- # TRANSPORT MODE (AUTO-CONFIGURED)
7
+ # MCP_TRANSPORT_TYPE: Toggles transport mode. Values: stdio | http | dual.
8
+ # Defaults to 'stdio' in development and 'dual' in production/NODE_ENV=production.
4
9
  # =============================================================================
5
- # When OAuth is configured, the server automatically runs in DUAL mode:
6
- # - STDIO: For MCP proto¯col communication with Studio/Claude
7
- # - HTTP: For OAuth metadata endpoints (/.well-known/oauth-protected-resource)
8
- # Both transports run simultaneously on different channels.
10
+ # MCP_TRANSPORT_TYPE=dual
11
+ # PORT=3000
12
+ # HOST=localhost
13
+ # ENABLE_CORS=true
14
+
15
+ # Streamable HTTP Session Limits (Optional)
9
16
  # =============================================================================
10
- # REQUIRED: Server Configuration
17
+ # Bounds memory against unauthenticated initialize floods. Defaults: 1000 sessions,
18
+ # 30-minute idle timeout. Only applies when running in http or dual transport mode.
19
+ # =============================================================================
20
+ # MCP_MAX_SESSIONS=1000
21
+ # MCP_SESSION_TIMEOUT_MS=1800000
22
+
23
+ # OAuth 2.1 MCP Server Configuration
11
24
  # =============================================================================
25
+ # Enforcement Gate (Optional)
12
26
  # =============================================================================
13
- # Auth0 Configuration
27
+ # OAUTH_REQUIRED controls whether authentication is enforced.
28
+ # - Unset / false (default): auth is NOT enforced. The server runs out-of-the-box
29
+ # and protected endpoints are reachable without a token. Best for local dev.
30
+ # - true: auth is enforced (fail-closed). If neither JWKS_URI nor
31
+ # INTROSPECTION_ENDPOINT is configured, the server STILL starts and logs a
32
+ # clear warning, but rejects all protected requests until a verifier is set.
33
+ # Set this to true (and configure a verifier below) before deploying.
34
+ # =============================================================================
35
+ # OAUTH_REQUIRED=true
36
+
37
+ # =============================================================================
38
+ # REQUIRED: Server Configuration
14
39
  # =============================================================================
15
- # After creating your API and Application in Auth0, fill in these values:
16
40
  # Your Auth0 API Identifier (from APIs → MCP Server → Identifier)
17
41
  # This MUST match exactly what you set when creating the API
18
- # This MUST match exactly what you set when creating the API
19
42
  RESOURCE_URI=https://mcplocal
20
- # Your Auth0 tenant domain (authorization server)
43
+
44
+ # Your Auth0 tenant domain (authorization server URL)
21
45
  AUTH_SERVER_URL=https://dev-5dt0utuk315713tjm.us.auth0.com
22
- # Expected token audience (should match RESOURCE_URI)
46
+
47
+ # Cryptographic JWKS Signature Verification (Recommended)
48
+ # =============================================================================
49
+ # If configured, the SDK cryptographically verifies token signatures using JWKS keys.
50
+ # Token validation is SECURE BY DEFAULT (fail-closed): if neither JWKS_URI nor an
51
+ # introspection endpoint (below) is configured, ALL tokens are rejected. Configure
52
+ # one of them for a working server.
53
+ # NOTE: Unsigned/signature-less token decoding is disabled by default and is only
54
+ # available via the in-code `allowInsecureTokenDecode: true` option in
55
+ # OAuthModule.forRoot(...) — intended for local development only, never production.
56
+ # =============================================================================
57
+ # JWKS_URI=https://dev-5dt0utuk315713tjm.us.auth0.com/.well-known/jwks.json
58
+
59
+ # Expected token validation audience (should match RESOURCE_URI)
23
60
  TOKEN_AUDIENCE=https://mcplocal
24
- # Expected token issuer (your Auth0 tenant domain with trailing slash)
61
+
62
+ # Expected token validation issuer (your authorization server tenant domain)
25
63
  TOKEN_ISSUER=https://dev-5dt0utuk315713tjm.us.auth0.com
26
64
 
27
- DUFFEL_API_KEY=duffel_test_-w0wGDHB0M3DU9k-sBeUbxLqwcibUQqfEbjWDTKNnly
65
+ # Token Introspection Configuration (Alternative to JWKS signature verification)
66
+ # =============================================================================
67
+ # INTROSPECTION_ENDPOINT=https://dev-5dt0utuk315713tjm.us.auth0.com/oauth/introspect
68
+ # INTROSPECTION_CLIENT_ID=your-introspection-client-id
69
+ # INTROSPECTION_CLIENT_SECRET=your-introspection-client-secret
70
+
71
+ # OAuth Discovery Settings (Optional)
72
+ # =============================================================================
73
+ # OAUTH_DISCOVERY_PORT=3005
74
+ # OAUTH_DISCOVERY_AUTO_RETRY=true
75
+ # MCP_SERVER_PORT=3000
76
+
77
+ # Dynamic Client Registration (Optional / disabled by default)
78
+ # =============================================================================
79
+ # Set to 'true' to expose the /oauth/v2/register endpoint. It only serves the
80
+ # statically configured client below; without OAUTH_CLIENT_ID it stays disabled.
81
+ # =============================================================================
82
+ # OAUTH_ENABLE_CLIENT_REGISTRATION=true
83
+ # OAUTH_CLIENT_ID=your-client-id
84
+ # OAUTH_CLIENT_SECRET=your-client-secret
85
+
86
+ # Duffel API Integration (Required for Flight Search & Booking)
87
+ # =============================================================================
88
+ DUFFEL_API_KEY=your-duffel-api-key
@@ -1,70 +1,56 @@
1
- # ✈️ NitroStack Flight Booking
1
+ # NitroStack OAuth Template
2
2
 
3
- A production-ready template showcasing real-time flight search and booking with **Duffel API** integration. Learn how to build complex, authenticated MCP servers with high-impact visual widgets.
3
+ Template for OAuth 2.1-enabled MCP servers, including auth-aware tool patterns
4
+ and a production-style project structure.
4
5
 
5
- ## Features
6
+ ## What This Template Includes
6
7
 
7
- - **Real-time Search** Powered by the Duffel API (300+ airlines).
8
- - **Seat Selection** Interactive visual cabin maps with seat pickers.
9
- - **Secure Payments** — Simulated secure booking and confirmation flows.
10
- - **Advanced Auth** — Optional OAuth 2.1 support for protected MCP servers.
8
+ - OAuth 2.1 oriented project setup
9
+ - Tool and module structure for protected workflows
10
+ - Environment-based configuration (`.env`)
11
+ - Widget-ready frontend integration
11
12
 
12
- ---
13
-
14
- ## 🚀 Quick Start
15
-
16
- ### 1. Initialize Your Project
17
-
18
- ```bash
19
- npx nitrostack init my-flight-app --template typescript-oauth
20
- cd my-flight-app
21
- ```
22
-
23
- ### 2. Install Dependencies
13
+ ## Quick Start
24
14
 
25
15
  ```bash
26
- npm run install:all
16
+ npx @nitrostack/cli init my-oauth-app --template typescript-oauth
17
+ cd my-oauth-app
18
+ npm run dev
27
19
  ```
28
20
 
29
- ### 3. Get NitroStudio
21
+ ## Configuration
30
22
 
31
- Experience the visual seat selection and flight search as your users would see it.
23
+ 1. Copy `.env.example` to `.env`
24
+ 2. Configure OAuth provider values
25
+ 3. (Optional) Configure provider-specific API keys
32
26
 
33
- ![NitroStudio](../../../../assets/gif/nitrostudio-main.gif)
27
+ For detailed setup, see `OAUTH_SETUP.md`.
34
28
 
35
- 1. **Download NitroStudio**: [nitrostack.ai/studio](https://nitrostack.ai/studio)
36
- 2. **Open Project**: Launch NitroStudio and select your project folder.
29
+ ## Common Commands
37
30
 
38
- ---
39
-
40
- ## ⚙️ Configuration
41
-
42
- ### 1. Duffel API Key
43
-
44
- You need a free Duffel API key to see live flight data:
31
+ ```bash
32
+ npm run dev
33
+ npm run build
34
+ npm start
35
+ ```
45
36
 
46
- 1. Sign up at [duffel.com](https://duffel.com/).
47
- 2. Copy your **Test Token** from the dashboard.
48
- 3. Create `.env` from the example:
49
- ```bash
50
- cp .env.example .env
51
- ```
52
- 4. Update `DUFFEL_API_KEY` in your `.env`.
37
+ ## NitroStudio
53
38
 
54
- ### 2. OAuth (Optional)
39
+ Use NitroStudio to test auth flows, inspect tool requests, and validate behavior.
55
40
 
56
- For production scenarios requiring user authentication, refer to [OAUTH_SETUP.md](./OAUTH_SETUP.md).
41
+ - Download: <https://nitrostack.ai/studio>
42
+ - Studio: <https://nitrostack.ai/studio>
57
43
 
58
- ---
44
+ ## Links
59
45
 
60
- ## 🛠️ Commands
46
+ - Docs: <https://docs.nitrostack.ai>
47
+ - OAuth docs: <https://docs.nitrostack.ai/sdk/typescript/11-oauth-authentication>
48
+ - Main repository: <https://github.com/nitrocloudofficial/nitrostack>
61
49
 
62
- - `npm run dev` — Start the development ecosystem.
63
- - `npm run build` — Bundle TypeScript and widgets for deployment.
64
- - `npm run upgrade` — Keep NitroStack core up to date.
50
+ ## Community
65
51
 
66
- ---
67
- **Official Resources**
68
- - [Website](https://nitrostack.ai)
69
- - [Docs](https://docs.nitrostack.ai)
70
- - [Duffel API](https://duffel.com/docs)
52
+ - Discord: <https://discord.gg/uVWey6UhuD>
53
+ - X: <https://x.com/nitrostackai>
54
+ - YouTube: <https://www.youtube.com/@nitrostackai>
55
+ - LinkedIn: <https://linkedin.com/company/nitrostack-ai/>
56
+ - GitHub: <https://github.com/nitrostackai>
@@ -19,7 +19,8 @@
19
19
  "dotenv": "^16.3.1",
20
20
  "@duffel/api": "^4.21.0",
21
21
  "axios": "^1.7.9",
22
- "date-fns": "^4.1.0"
22
+ "date-fns": "^4.1.0",
23
+ "@modelcontextprotocol/ext-apps": ">=0.1.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@nitrostack/cli": "^1",
@@ -36,6 +36,13 @@ import { SystemHealthCheck } from './health/system.health.js';
36
36
 
37
37
  // Enable OAuth 2.1 authentication
38
38
  OAuthModule.forRoot({
39
+ // Whether OAuth is enforced. Defaults to false (dev-friendly): the server
40
+ // runs out-of-the-box and protected endpoints are reachable without a token.
41
+ // Set OAUTH_REQUIRED=true to enforce auth (fail-closed). When enforced but no
42
+ // verifier (JWKS_URI / INTROSPECTION_ENDPOINT) is configured, the server still
43
+ // starts and warns, but rejects protected requests until one is configured.
44
+ required: process.env.OAUTH_REQUIRED === 'true',
45
+
39
46
  // Resource URI - YOUR MCP server's public URL
40
47
  // This is used for token audience binding (RFC 8707)
41
48
  // CRITICAL: Tokens must be issued specifically for this URI