nitrostack 1.0.55 → 1.0.56
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/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +14 -9
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/widgets/hooks/index.d.ts +7 -0
- package/dist/widgets/hooks/index.d.ts.map +1 -0
- package/dist/widgets/hooks/index.js +7 -0
- package/dist/widgets/hooks/index.js.map +1 -0
- package/dist/widgets/hooks/use-display-mode.d.ts +11 -0
- package/dist/widgets/hooks/use-display-mode.d.ts.map +1 -0
- package/dist/widgets/hooks/use-display-mode.js +13 -0
- package/dist/widgets/hooks/use-display-mode.js.map +1 -0
- package/dist/widgets/hooks/use-max-height.d.ts +10 -0
- package/dist/widgets/hooks/use-max-height.d.ts.map +1 -0
- package/dist/widgets/hooks/use-max-height.js +13 -0
- package/dist/widgets/hooks/use-max-height.js.map +1 -0
- package/dist/widgets/hooks/use-openai-global.d.ts +12 -0
- package/dist/widgets/hooks/use-openai-global.d.ts.map +1 -0
- package/dist/widgets/hooks/use-openai-global.js +32 -0
- package/dist/widgets/hooks/use-openai-global.js.map +1 -0
- package/dist/widgets/hooks/use-theme.d.ts +10 -0
- package/dist/widgets/hooks/use-theme.d.ts.map +1 -0
- package/dist/widgets/hooks/use-theme.js +12 -0
- package/dist/widgets/hooks/use-theme.js.map +1 -0
- package/dist/widgets/hooks/use-widget-state.d.ts +18 -0
- package/dist/widgets/hooks/use-widget-state.d.ts.map +1 -0
- package/dist/widgets/hooks/use-widget-state.js +27 -0
- package/dist/widgets/hooks/use-widget-state.js.map +1 -0
- package/dist/widgets/hooks/useWidgetSDK.d.ts +47 -0
- package/dist/widgets/hooks/useWidgetSDK.d.ts.map +1 -0
- package/dist/widgets/hooks/useWidgetSDK.js +67 -0
- package/dist/widgets/hooks/useWidgetSDK.js.map +1 -0
- package/dist/widgets/index.d.ts +7 -1
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +11 -1
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/runtime/WidgetLayout.d.ts +32 -0
- package/dist/widgets/runtime/WidgetLayout.d.ts.map +1 -0
- package/dist/widgets/runtime/WidgetLayout.js +143 -0
- package/dist/widgets/runtime/WidgetLayout.js.map +1 -0
- package/dist/widgets/runtime/widget-polyfill.d.ts +1 -0
- package/dist/widgets/runtime/widget-polyfill.d.ts.map +1 -0
- package/dist/widgets/runtime/widget-polyfill.js +28 -0
- package/dist/widgets/runtime/widget-polyfill.js.map +1 -0
- package/dist/widgets/sdk.d.ts +109 -0
- package/dist/widgets/sdk.d.ts.map +1 -0
- package/dist/widgets/sdk.js +221 -0
- package/dist/widgets/sdk.js.map +1 -0
- package/dist/widgets/types.d.ts +89 -0
- package/dist/widgets/types.d.ts.map +1 -0
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/types.js.map +1 -0
- package/dist/widgets/utils/media-queries.d.ts +34 -0
- package/dist/widgets/utils/media-queries.d.ts.map +1 -0
- package/dist/widgets/utils/media-queries.js +42 -0
- package/dist/widgets/utils/media-queries.js.map +1 -0
- package/package.json +1 -1
- package/src/studio/app/chat/page.tsx +274 -137
- package/src/studio/app/globals.css +140 -64
- package/src/studio/branding.md +807 -0
- package/src/studio/components/WidgetRenderer.tsx +222 -16
- package/src/studio/lib/llm-service.ts +39 -39
- package/templates/typescript-oauth/{env.example → .env.example} +4 -10
- package/templates/typescript-oauth/README.md +226 -306
- package/templates/typescript-oauth/package-lock.json +4253 -0
- package/templates/typescript-oauth/package.json +10 -5
- package/templates/typescript-oauth/src/app.module.ts +39 -36
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +0 -1
- package/templates/typescript-oauth/src/index.ts +22 -30
- package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
- package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
- package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +231 -0
- package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
- package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
- package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
- package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
- package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
- package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
- package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +6 -2
- package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
- package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
- package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
- package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
- package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
- package/templates/typescript-oauth/src/widgets/package-lock.json +155 -126
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +374 -27
- package/templates/typescript-pizzaz/IMPLEMENTATION.md +98 -0
- package/templates/typescript-pizzaz/README.md +233 -0
- package/templates/typescript-pizzaz/package.json +31 -0
- package/templates/typescript-pizzaz/src/app.module.ts +28 -0
- package/templates/typescript-pizzaz/src/index.ts +30 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
- package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
- package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
- package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
- package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
- package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
- package/templates/typescript-pizzaz/tsconfig.json +30 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +0 -1
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +102 -56
- package/templates/typescript-starter/src/widgets/app/layout.tsx +6 -2
- package/templates/typescript-auth/AI_AGENT_CLI_REFERENCE.md +0 -702
- package/templates/typescript-auth/AI_AGENT_SDK_REFERENCE.md +0 -1260
- package/templates/typescript-auth/README.md +0 -402
- package/templates/typescript-auth/env.example +0 -25
- package/templates/typescript-auth/package.json +0 -36
- package/templates/typescript-auth/src/app.module.ts +0 -103
- package/templates/typescript-auth/src/db/database.ts +0 -160
- package/templates/typescript-auth/src/db/seed.ts +0 -374
- package/templates/typescript-auth/src/db/setup.ts +0 -87
- package/templates/typescript-auth/src/events/analytics.service.ts +0 -52
- package/templates/typescript-auth/src/events/notification.service.ts +0 -40
- package/templates/typescript-auth/src/filters/global-exception.filter.ts +0 -28
- package/templates/typescript-auth/src/guards/README.md +0 -75
- package/templates/typescript-auth/src/guards/jwt.guard.ts +0 -105
- package/templates/typescript-auth/src/health/database.health.ts +0 -41
- package/templates/typescript-auth/src/index.ts +0 -29
- package/templates/typescript-auth/src/interceptors/transform.interceptor.ts +0 -24
- package/templates/typescript-auth/src/middleware/logging.middleware.ts +0 -42
- package/templates/typescript-auth/src/modules/addresses/addresses.module.ts +0 -16
- package/templates/typescript-auth/src/modules/addresses/addresses.prompts.ts +0 -114
- package/templates/typescript-auth/src/modules/addresses/addresses.resources.ts +0 -40
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +0 -284
- package/templates/typescript-auth/src/modules/auth/auth.module.ts +0 -16
- package/templates/typescript-auth/src/modules/auth/auth.prompts.ts +0 -147
- package/templates/typescript-auth/src/modules/auth/auth.resources.ts +0 -84
- package/templates/typescript-auth/src/modules/auth/auth.tools.ts +0 -139
- package/templates/typescript-auth/src/modules/cart/cart.module.ts +0 -16
- package/templates/typescript-auth/src/modules/cart/cart.prompts.ts +0 -95
- package/templates/typescript-auth/src/modules/cart/cart.resources.ts +0 -44
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +0 -277
- package/templates/typescript-auth/src/modules/orders/orders.module.ts +0 -16
- package/templates/typescript-auth/src/modules/orders/orders.prompts.ts +0 -88
- package/templates/typescript-auth/src/modules/orders/orders.resources.ts +0 -48
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +0 -303
- package/templates/typescript-auth/src/modules/products/products.module.ts +0 -16
- package/templates/typescript-auth/src/modules/products/products.prompts.ts +0 -146
- package/templates/typescript-auth/src/modules/products/products.resources.ts +0 -98
- package/templates/typescript-auth/src/modules/products/products.tools.ts +0 -266
- package/templates/typescript-auth/src/pipes/validation.pipe.ts +0 -42
- package/templates/typescript-auth/src/services/database.service.ts +0 -90
- package/templates/typescript-auth/src/widgets/app/add-to-cart/page.tsx +0 -122
- package/templates/typescript-auth/src/widgets/app/address-added/page.tsx +0 -116
- package/templates/typescript-auth/src/widgets/app/address-deleted/page.tsx +0 -105
- package/templates/typescript-auth/src/widgets/app/address-list/page.tsx +0 -139
- package/templates/typescript-auth/src/widgets/app/address-updated/page.tsx +0 -153
- package/templates/typescript-auth/src/widgets/app/cart-cleared/page.tsx +0 -86
- package/templates/typescript-auth/src/widgets/app/cart-updated/page.tsx +0 -116
- package/templates/typescript-auth/src/widgets/app/categories/page.tsx +0 -134
- package/templates/typescript-auth/src/widgets/app/layout.tsx +0 -21
- package/templates/typescript-auth/src/widgets/app/login-result/page.tsx +0 -129
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +0 -231
- package/templates/typescript-auth/src/widgets/app/order-details/page.tsx +0 -225
- package/templates/typescript-auth/src/widgets/app/order-history/page.tsx +0 -218
- package/templates/typescript-auth/src/widgets/app/product-card/page.tsx +0 -121
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +0 -198
- package/templates/typescript-auth/src/widgets/app/shopping-cart/page.tsx +0 -187
- package/templates/typescript-auth/src/widgets/app/whoami/page.tsx +0 -165
- package/templates/typescript-auth/src/widgets/next.config.js +0 -38
- package/templates/typescript-auth/src/widgets/package.json +0 -18
- package/templates/typescript-auth/src/widgets/styles/ecommerce.ts +0 -169
- package/templates/typescript-auth/src/widgets/tsconfig.json +0 -28
- package/templates/typescript-auth/src/widgets/types/tool-data.ts +0 -141
- package/templates/typescript-auth/src/widgets/widget-manifest.json +0 -464
- package/templates/typescript-auth/tsconfig.json +0 -27
- package/templates/typescript-auth-api-key/AI_AGENT_CLI_REFERENCE.md +0 -701
- package/templates/typescript-auth-api-key/AI_AGENT_SDK_REFERENCE.md +0 -1260
- package/templates/typescript-auth-api-key/README.md +0 -485
- package/templates/typescript-auth-api-key/env.example +0 -17
- package/templates/typescript-auth-api-key/package.json +0 -21
- package/templates/typescript-auth-api-key/src/app.module.ts +0 -38
- package/templates/typescript-auth-api-key/src/guards/apikey.guard.ts +0 -47
- package/templates/typescript-auth-api-key/src/guards/multi-auth.guard.ts +0 -157
- package/templates/typescript-auth-api-key/src/index.ts +0 -47
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.module.ts +0 -12
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.prompts.ts +0 -73
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.resources.ts +0 -60
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.tools.ts +0 -71
- package/templates/typescript-auth-api-key/src/modules/demo/demo.module.ts +0 -18
- package/templates/typescript-auth-api-key/src/modules/demo/demo.tools.ts +0 -155
- package/templates/typescript-auth-api-key/src/modules/demo/multi-auth.tools.ts +0 -123
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-operations/page.tsx +0 -133
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-result/page.tsx +0 -134
- package/templates/typescript-auth-api-key/src/widgets/app/layout.tsx +0 -14
- package/templates/typescript-auth-api-key/src/widgets/package.json +0 -24
- package/templates/typescript-auth-api-key/src/widgets/widget-manifest.json +0 -48
- package/templates/typescript-auth-api-key/tsconfig.json +0 -23
- package/templates/typescript-oauth/OAUTH_SETUP.md +0 -592
- package/templates/typescript-oauth/src/modules/demo/demo.module.ts +0 -16
- package/templates/typescript-oauth/src/modules/demo/demo.tools.ts +0 -190
- package/templates/typescript-oauth/src/widgets/app/calculator-operations/page.tsx +0 -133
- package/templates/typescript-oauth/src/widgets/app/calculator-result/page.tsx +0 -134
- package/templates/typescript-oauth/src/widgets/out/404.html +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/WU9THacVqL52RZbrZOLS1/_buildManifest.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/WU9THacVqL52RZbrZOLS1/_ssgManifest.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/117-eb57c7ef86f964a4.js +0 -2
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/app/_not-found/page-dcb83ba3e4d0aafd.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/app/calculator-operations/page-b8913a740073ea8a.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/app/calculator-result/page-ddaaab2fce95dea2.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/app/layout-cbd3ebdc4ecc5247.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/fd9d1056-749e5812300142af.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/main-76df43fcef3db344.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/main-app-f9c40224d04023c5.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/_next/static/chunks/webpack-100b9e646d9c912e.js +0 -1
- package/templates/typescript-oauth/src/widgets/out/calculator-operations.html +0 -1
- package/templates/typescript-oauth/src/widgets/out/calculator-operations.txt +0 -7
- package/templates/typescript-oauth/src/widgets/out/calculator-result.html +0 -1
- package/templates/typescript-oauth/src/widgets/out/calculator-result.txt +0 -7
- package/templates/typescript-starter/src/widgets/app/calculator-operations/page.tsx +0 -133
- /package/templates/{typescript-auth-api-key → typescript-oauth}/src/health/system.health.ts +0 -0
- /package/templates/{typescript-auth-api-key → typescript-pizzaz}/src/widgets/next.config.js +0 -0
- /package/templates/{typescript-auth-api-key → typescript-pizzaz}/src/widgets/tsconfig.json +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "typescript-oauth",
|
|
2
|
+
"name": "nitrostack-typescript-oauth",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
"widget": "npm --prefix src/widgets"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"dotenv": "^16.4.5",
|
|
15
14
|
"nitrostack": "^1",
|
|
16
|
-
"zod": "^3.
|
|
15
|
+
"zod": "^3.22.4",
|
|
16
|
+
"dotenv": "^16.3.1",
|
|
17
|
+
"@duffel/api": "^4.21.0",
|
|
18
|
+
"axios": "^1.7.9",
|
|
19
|
+
"date-fns": "^4.1.0"
|
|
17
20
|
},
|
|
18
21
|
"devDependencies": {
|
|
19
22
|
"typescript": "^5.3.3"
|
|
20
|
-
}
|
|
21
|
-
|
|
23
|
+
},
|
|
24
|
+
"description": "NitroStack OAuth 2.1 template with flight booking - demonstrates authentication, API integration, and modern widget development",
|
|
25
|
+
"author": ""
|
|
26
|
+
}
|
|
@@ -1,49 +1,52 @@
|
|
|
1
|
-
import { McpApp, Module, OAuthModule } from 'nitrostack';
|
|
2
|
-
import {
|
|
1
|
+
import { McpApp, Module, ConfigModule, OAuthModule } from 'nitrostack';
|
|
2
|
+
import { FlightsModule } from './modules/flights/flights.module.js';
|
|
3
|
+
import { SystemHealthCheck } from './health/system.health.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Root Application Module
|
|
6
7
|
*
|
|
7
|
-
* This module
|
|
8
|
-
*
|
|
9
|
-
* - OpenAI Apps SDK Requirements: https://developers.openai.com/apps-sdk/build/auth
|
|
8
|
+
* This is the main module that bootstraps the MCP server.
|
|
9
|
+
* It registers all feature modules and health checks.
|
|
10
10
|
*
|
|
11
|
-
* OAuth 2.1
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
11
|
+
* OAuth 2.1 Authentication:
|
|
12
|
+
* - Configured with Auth0 as the authorization server
|
|
13
|
+
* - Supports read, write, and admin scopes
|
|
14
|
+
* - Validates tokens with audience binding (RFC 8707)
|
|
15
|
+
*
|
|
16
|
+
* Flight Booking System:
|
|
17
|
+
* - Powered by Duffel API
|
|
18
|
+
* - Professional flight search and booking capabilities
|
|
19
|
+
* - Comprehensive widgets for search results and flight details
|
|
19
20
|
*/
|
|
20
21
|
@McpApp({
|
|
21
22
|
module: AppModule,
|
|
22
23
|
server: {
|
|
23
|
-
name: '
|
|
24
|
-
version: '1.0.0'
|
|
24
|
+
name: 'airline-ticketing-server',
|
|
25
|
+
version: '1.0.0'
|
|
25
26
|
},
|
|
26
27
|
logging: {
|
|
27
|
-
level: 'info'
|
|
28
|
-
}
|
|
28
|
+
level: 'info'
|
|
29
|
+
}
|
|
29
30
|
})
|
|
30
31
|
@Module({
|
|
31
32
|
name: 'app',
|
|
32
|
-
description: '
|
|
33
|
+
description: 'Airline ticketing MCP server with OAuth 2.1 authentication and Duffel integration',
|
|
33
34
|
imports: [
|
|
35
|
+
ConfigModule.forRoot(),
|
|
36
|
+
|
|
34
37
|
// Enable OAuth 2.1 authentication
|
|
35
38
|
OAuthModule.forRoot({
|
|
36
39
|
// Resource URI - YOUR MCP server's public URL
|
|
37
40
|
// This is used for token audience binding (RFC 8707)
|
|
38
41
|
// CRITICAL: Tokens must be issued specifically for this URI
|
|
39
|
-
resourceUri: process.env.RESOURCE_URI || 'https://
|
|
40
|
-
|
|
42
|
+
resourceUri: process.env.RESOURCE_URI || 'https://mcplocal',
|
|
43
|
+
|
|
41
44
|
// Authorization Server(s) - The OAuth provider URL(s)
|
|
42
45
|
// Supports multiple auth servers for federation scenarios
|
|
43
46
|
authorizationServers: [
|
|
44
|
-
process.env.AUTH_SERVER_URL || 'https://
|
|
47
|
+
process.env.AUTH_SERVER_URL || 'https://dev-5dt0utuk31h13tjm.us.auth0.com',
|
|
45
48
|
],
|
|
46
|
-
|
|
49
|
+
|
|
47
50
|
// Supported scopes for this MCP server
|
|
48
51
|
// Define what permissions your server supports
|
|
49
52
|
scopesSupported: [
|
|
@@ -51,39 +54,39 @@ import { DemoModule } from './modules/demo/demo.module.js';
|
|
|
51
54
|
'write', // Write/modify resources
|
|
52
55
|
'admin', // Administrative operations
|
|
53
56
|
],
|
|
54
|
-
|
|
57
|
+
|
|
55
58
|
// Token Introspection (RFC 7662) - For opaque tokens
|
|
56
59
|
// If your OAuth provider issues opaque tokens (not JWTs),
|
|
57
60
|
// you MUST configure introspection to validate them
|
|
58
61
|
tokenIntrospectionEndpoint: process.env.INTROSPECTION_ENDPOINT,
|
|
59
62
|
tokenIntrospectionClientId: process.env.INTROSPECTION_CLIENT_ID,
|
|
60
63
|
tokenIntrospectionClientSecret: process.env.INTROSPECTION_CLIENT_SECRET,
|
|
61
|
-
|
|
64
|
+
|
|
62
65
|
// Expected audience (defaults to resourceUri if not provided)
|
|
63
66
|
// MUST match the audience claim in tokens (RFC 8707)
|
|
64
67
|
audience: process.env.TOKEN_AUDIENCE,
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
// Expected issuer (optional but recommended)
|
|
67
70
|
// If provided, tokens must be from this issuer
|
|
68
71
|
issuer: process.env.TOKEN_ISSUER,
|
|
69
|
-
|
|
72
|
+
|
|
70
73
|
// Custom validation (optional)
|
|
71
74
|
// Add any additional validation logic beyond spec requirements
|
|
72
75
|
customValidation: async (tokenPayload) => {
|
|
73
76
|
// Example: Check if user is active in your database
|
|
74
77
|
// const user = await db.users.findOne({ id: tokenPayload.sub });
|
|
75
78
|
// return user?.active === true;
|
|
76
|
-
|
|
77
|
-
// For
|
|
79
|
+
|
|
80
|
+
// For now, accept all valid tokens
|
|
78
81
|
return true;
|
|
79
82
|
},
|
|
80
83
|
}),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
DemoModule,
|
|
84
|
+
|
|
85
|
+
FlightsModule
|
|
84
86
|
],
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
providers: [
|
|
88
|
+
// Health Checks
|
|
89
|
+
SystemHealthCheck,
|
|
90
|
+
]
|
|
87
91
|
})
|
|
88
|
-
export class AppModule {}
|
|
89
|
-
|
|
92
|
+
export class AppModule { }
|
|
@@ -1,71 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import 'dotenv/config';
|
|
3
|
-
import { McpApplicationFactory } from 'nitrostack';
|
|
4
|
-
import { AppModule } from './app.module.js';
|
|
5
|
-
|
|
6
2
|
/**
|
|
7
|
-
* OAuth 2.1
|
|
3
|
+
* Calculator MCP Server with OAuth 2.1 Authentication
|
|
8
4
|
*
|
|
9
|
-
*
|
|
5
|
+
* Main entry point for the MCP server.
|
|
6
|
+
* Uses the @McpApp decorator pattern for clean, NestJS-style architecture.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
8
|
+
* OAuth 2.1 Compliance:
|
|
12
9
|
* - MCP Specification: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
13
10
|
* - OpenAI Apps SDK: https://developers.openai.com/apps-sdk/build/auth
|
|
14
|
-
*
|
|
15
|
-
* Standards:
|
|
16
|
-
* - OAuth 2.1 (draft-ietf-oauth-v2-1-13)
|
|
17
11
|
* - RFC 9728 - Protected Resource Metadata
|
|
18
|
-
* - RFC 8414 - Authorization Server Metadata
|
|
19
|
-
* - RFC 7591 - Dynamic Client Registration
|
|
20
12
|
* - RFC 8707 - Resource Indicators (Token Audience Binding)
|
|
21
|
-
* - RFC 7636 - PKCE
|
|
22
|
-
* - RFC 7662 - Token Introspection
|
|
23
|
-
*
|
|
24
|
-
* Compatible with:
|
|
25
|
-
* - Auth0
|
|
26
|
-
* - Okta
|
|
27
|
-
* - Keycloak
|
|
28
|
-
* - Any RFC-compliant OAuth 2.1 provider
|
|
29
13
|
*
|
|
30
14
|
* Transport Configuration:
|
|
31
15
|
* - Development (NODE_ENV=development): STDIO only
|
|
32
16
|
* - Production (NODE_ENV=production): Dual transport (STDIO + HTTP SSE)
|
|
17
|
+
* - With OAuth: Dual mode (STDIO + HTTP for metadata endpoints)
|
|
33
18
|
*/
|
|
34
19
|
|
|
20
|
+
import 'dotenv/config';
|
|
21
|
+
import { McpApplicationFactory } from 'nitrostack';
|
|
22
|
+
import { AppModule } from './app.module.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bootstrap the application
|
|
26
|
+
*/
|
|
35
27
|
async function bootstrap() {
|
|
36
28
|
try {
|
|
37
|
-
console.error('🔐 Starting OAuth 2.1
|
|
29
|
+
console.error('🔐 Starting Calculator MCP Server with OAuth 2.1...\\n');
|
|
38
30
|
|
|
39
|
-
// Validate required environment variables
|
|
31
|
+
// Validate required environment variables for OAuth
|
|
40
32
|
const requiredEnvVars = ['RESOURCE_URI', 'AUTH_SERVER_URL'];
|
|
41
33
|
const missing = requiredEnvVars.filter(v => !process.env[v]);
|
|
42
34
|
|
|
43
35
|
if (missing.length > 0) {
|
|
44
|
-
console.error('❌ Missing required environment variables:');
|
|
36
|
+
console.error('❌ Missing required OAuth environment variables:');
|
|
45
37
|
missing.forEach(v => console.error(` - ${v}`));
|
|
46
|
-
console.error('
|
|
47
|
-
console.error('
|
|
38
|
+
console.error('\\n💡 Copy .env.example to .env and configure your OAuth provider');
|
|
39
|
+
console.error(' Or check the test-oauth/.env for reference\\n');
|
|
48
40
|
process.exit(1);
|
|
49
41
|
}
|
|
50
42
|
|
|
51
43
|
// Create the MCP application
|
|
52
|
-
const
|
|
44
|
+
const server = await McpApplicationFactory.create(AppModule);
|
|
53
45
|
|
|
54
46
|
console.error('✅ OAuth 2.1 Module configured');
|
|
55
47
|
console.error(` Resource URI: ${process.env.RESOURCE_URI}`);
|
|
56
48
|
console.error(` Auth Server: ${process.env.AUTH_SERVER_URL}`);
|
|
57
49
|
console.error(` Scopes: read, write, admin`);
|
|
58
|
-
console.error(` Audience: ${process.env.TOKEN_AUDIENCE || process.env.RESOURCE_URI}
|
|
50
|
+
console.error(` Audience: ${process.env.TOKEN_AUDIENCE || process.env.RESOURCE_URI}\\n`);
|
|
59
51
|
|
|
60
52
|
// Start the server
|
|
61
|
-
await
|
|
53
|
+
await server.start();
|
|
62
54
|
|
|
63
55
|
} catch (error) {
|
|
64
56
|
console.error('❌ Failed to start server:', error);
|
|
65
|
-
console.error('
|
|
66
|
-
console.error(' See OAUTH_SETUP.md for troubleshooting\n');
|
|
57
|
+
console.error('\\n💡 Check your OAuth configuration in .env\\n');
|
|
67
58
|
process.exit(1);
|
|
68
59
|
}
|
|
69
60
|
}
|
|
70
61
|
|
|
62
|
+
// Start the application
|
|
71
63
|
bootstrap();
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { ToolDecorator as Tool, Widget, ExecutionContext, z, UseGuards, Injectable } from 'nitrostack';
|
|
2
|
+
import { OAuthGuard } from '../../guards/oauth.guard.js';
|
|
3
|
+
import { DuffelService } from '../../services/duffel.service.js';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class BookingTools {
|
|
7
|
+
constructor(private duffelService: DuffelService) { }
|
|
8
|
+
|
|
9
|
+
@Tool({
|
|
10
|
+
name: 'create_order',
|
|
11
|
+
description: 'Create a flight order with hold (no payment required). IMPORTANT: Before calling this tool, you MUST collect passenger information from the user. Ask for: full name (first and last), title (Mr/Ms/Mrs/Miss/Dr), gender (M/F), date of birth (YYYY-MM-DD), email, and phone number with country code. The order will be held for later payment.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
offerId: z.string().describe('The offer ID to book'),
|
|
14
|
+
passengers: z.string().describe('JSON string containing array of passenger objects. Each passenger must have: title (mr/ms/mrs/miss/dr), givenName (first name), familyName (last name), gender (M/F), bornOn (YYYY-MM-DD), email, phoneNumber. Example: \'[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john@example.com","phoneNumber":"+1234567890"}]\'')
|
|
15
|
+
}),
|
|
16
|
+
examples: {
|
|
17
|
+
request: {
|
|
18
|
+
offerId: 'off_123456',
|
|
19
|
+
passengers: '[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john.doe@example.com","phoneNumber":"+1234567890"}]'
|
|
20
|
+
},
|
|
21
|
+
response: {
|
|
22
|
+
orderId: 'ord_123456',
|
|
23
|
+
status: 'held',
|
|
24
|
+
totalAmount: '450.00',
|
|
25
|
+
totalCurrency: 'USD',
|
|
26
|
+
expiresAt: '2024-03-01T12:00:00Z',
|
|
27
|
+
passengers: [],
|
|
28
|
+
slices: [],
|
|
29
|
+
message: 'Order created and held successfully.'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
@UseGuards(OAuthGuard)
|
|
34
|
+
@Widget('order-summary')
|
|
35
|
+
async createOrder(input: any, ctx: ExecutionContext) {
|
|
36
|
+
ctx.logger.info('Creating flight order (hold)', {
|
|
37
|
+
user: ctx.auth?.subject,
|
|
38
|
+
offerId: input.offerId
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Validate and parse passengers
|
|
42
|
+
let passengersArray;
|
|
43
|
+
try {
|
|
44
|
+
if (typeof input.passengers === 'string') {
|
|
45
|
+
// Try to parse the JSON string
|
|
46
|
+
// Handle both regular JSON and double-encoded JSON
|
|
47
|
+
let passengerStr = input.passengers;
|
|
48
|
+
|
|
49
|
+
// If the string starts with escaped quotes, it might be double-encoded
|
|
50
|
+
if (passengerStr.startsWith('\\"') || passengerStr.includes('\\"')) {
|
|
51
|
+
// Remove escape characters
|
|
52
|
+
passengerStr = passengerStr.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
passengersArray = JSON.parse(passengerStr);
|
|
56
|
+
} else if (Array.isArray(input.passengers)) {
|
|
57
|
+
passengersArray = input.passengers;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Passengers must be a JSON string or array');
|
|
60
|
+
}
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
ctx.logger.error('Failed to parse passengers', {
|
|
63
|
+
input: input.passengers,
|
|
64
|
+
error: error.message
|
|
65
|
+
});
|
|
66
|
+
throw new Error(`Invalid passengers format: ${error.message}. Expected JSON string like '[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john@example.com","phoneNumber":"+1234567890"}]'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!passengersArray || !Array.isArray(passengersArray) || passengersArray.length === 0) {
|
|
70
|
+
throw new Error('At least one passenger is required to create an order');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Transform passengers to Duffel format
|
|
74
|
+
// Pass inline passenger data - Duffel will create passenger records automatically
|
|
75
|
+
const passengers = passengersArray.map((pax: any) => ({
|
|
76
|
+
title: pax.title,
|
|
77
|
+
given_name: pax.givenName,
|
|
78
|
+
family_name: pax.familyName,
|
|
79
|
+
gender: pax.gender,
|
|
80
|
+
born_on: pax.bornOn,
|
|
81
|
+
email: pax.email,
|
|
82
|
+
phone_number: pax.phoneNumber
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const orderParams: any = {
|
|
86
|
+
selectedOffers: [input.offerId],
|
|
87
|
+
passengers,
|
|
88
|
+
type: 'hold' // Always create hold orders
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const order = await this.duffelService.createOrder(orderParams);
|
|
92
|
+
|
|
93
|
+
ctx.logger.info('Order created successfully', {
|
|
94
|
+
user: ctx.auth?.subject,
|
|
95
|
+
orderId: order.id,
|
|
96
|
+
status: 'held'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
orderId: order.id,
|
|
101
|
+
status: 'held',
|
|
102
|
+
totalAmount: order.total_amount,
|
|
103
|
+
totalCurrency: order.total_currency,
|
|
104
|
+
expiresAt: (order as any).expires_at,
|
|
105
|
+
bookingReference: order.booking_reference,
|
|
106
|
+
passengers: order.passengers.map((pax: any) => ({
|
|
107
|
+
id: pax.id,
|
|
108
|
+
name: `${pax.given_name} ${pax.family_name}`,
|
|
109
|
+
type: pax.type
|
|
110
|
+
})),
|
|
111
|
+
slices: order.slices.map((slice: any) => ({
|
|
112
|
+
origin: slice.origin.iata_code,
|
|
113
|
+
destination: slice.destination.iata_code,
|
|
114
|
+
departureTime: slice.segments[0].departing_at,
|
|
115
|
+
arrivalTime: slice.segments[slice.segments.length - 1].arriving_at
|
|
116
|
+
})),
|
|
117
|
+
message: 'Order created and held successfully.'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@Tool({
|
|
124
|
+
name: 'get_order_details',
|
|
125
|
+
description: 'Get detailed information about an order',
|
|
126
|
+
inputSchema: z.object({
|
|
127
|
+
orderId: z.string().describe('The order ID')
|
|
128
|
+
}),
|
|
129
|
+
examples: {
|
|
130
|
+
request: {
|
|
131
|
+
orderId: 'ord_123456'
|
|
132
|
+
},
|
|
133
|
+
response: {
|
|
134
|
+
orderId: 'ord_123456',
|
|
135
|
+
status: 'confirmed',
|
|
136
|
+
bookingReference: 'ABC123',
|
|
137
|
+
totalAmount: '450.00',
|
|
138
|
+
totalCurrency: 'USD',
|
|
139
|
+
passengers: [],
|
|
140
|
+
slices: []
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
@UseGuards(OAuthGuard)
|
|
145
|
+
@Widget('order-summary')
|
|
146
|
+
async getOrderDetails(input: any, ctx: ExecutionContext) {
|
|
147
|
+
ctx.logger.info('Getting order details', {
|
|
148
|
+
user: ctx.auth?.subject,
|
|
149
|
+
orderId: input.orderId
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const order = await this.duffelService.getOrder(input.orderId);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
orderId: order.id,
|
|
156
|
+
status: (order as any).status || 'confirmed',
|
|
157
|
+
bookingReference: order.booking_reference,
|
|
158
|
+
totalAmount: order.total_amount,
|
|
159
|
+
totalCurrency: order.total_currency,
|
|
160
|
+
createdAt: order.created_at,
|
|
161
|
+
expiresAt: (order as any).expires_at,
|
|
162
|
+
passengers: order.passengers.map((pax: any) => ({
|
|
163
|
+
id: pax.id,
|
|
164
|
+
name: `${pax.given_name} ${pax.family_name}`,
|
|
165
|
+
type: pax.type,
|
|
166
|
+
email: pax.email,
|
|
167
|
+
phoneNumber: pax.phone_number
|
|
168
|
+
})),
|
|
169
|
+
slices: order.slices.map((slice: any) => ({
|
|
170
|
+
id: slice.id,
|
|
171
|
+
origin: {
|
|
172
|
+
code: slice.origin.iata_code,
|
|
173
|
+
name: slice.origin.name,
|
|
174
|
+
city: slice.origin.city_name
|
|
175
|
+
},
|
|
176
|
+
destination: {
|
|
177
|
+
code: slice.destination.iata_code,
|
|
178
|
+
name: slice.destination.name,
|
|
179
|
+
city: slice.destination.city_name
|
|
180
|
+
},
|
|
181
|
+
duration: slice.duration,
|
|
182
|
+
segments: slice.segments.map((seg: any) => ({
|
|
183
|
+
id: seg.id,
|
|
184
|
+
origin: seg.origin.iata_code,
|
|
185
|
+
destination: seg.destination.iata_code,
|
|
186
|
+
departingAt: seg.departing_at,
|
|
187
|
+
arrivingAt: seg.arriving_at,
|
|
188
|
+
airline: seg.marketing_carrier.name,
|
|
189
|
+
flightNumber: seg.marketing_carrier_flight_number,
|
|
190
|
+
aircraft: seg.aircraft?.name
|
|
191
|
+
}))
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Tool({
|
|
197
|
+
name: 'get_seat_map',
|
|
198
|
+
description: 'Get available seats for a flight offer to allow seat selection',
|
|
199
|
+
inputSchema: z.object({
|
|
200
|
+
offerId: z.string().describe('The offer ID to get seats for')
|
|
201
|
+
}),
|
|
202
|
+
examples: {
|
|
203
|
+
request: {
|
|
204
|
+
offerId: 'off_123456'
|
|
205
|
+
},
|
|
206
|
+
response: {
|
|
207
|
+
offerId: 'off_123456',
|
|
208
|
+
cabins: [
|
|
209
|
+
{
|
|
210
|
+
cabinClass: 'economy',
|
|
211
|
+
rows: [
|
|
212
|
+
{
|
|
213
|
+
rowNumber: 10,
|
|
214
|
+
seats: [
|
|
215
|
+
{
|
|
216
|
+
id: 'seat_10a',
|
|
217
|
+
column: 'A',
|
|
218
|
+
available: true,
|
|
219
|
+
price: '25.00',
|
|
220
|
+
currency: 'USD',
|
|
221
|
+
type: 'window'
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'seat_10b',
|
|
225
|
+
column: 'B',
|
|
226
|
+
available: true,
|
|
227
|
+
price: '0',
|
|
228
|
+
currency: 'USD',
|
|
229
|
+
type: 'middle'
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'seat_10c',
|
|
233
|
+
column: 'C',
|
|
234
|
+
available: true,
|
|
235
|
+
price: '15.00',
|
|
236
|
+
currency: 'USD',
|
|
237
|
+
type: 'aisle'
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
message: 'Select your preferred seats from the available options'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
@UseGuards(OAuthGuard)
|
|
249
|
+
@Widget('seat-selection')
|
|
250
|
+
async getSeatMap(input: any, ctx: ExecutionContext) {
|
|
251
|
+
ctx.logger.info('Getting seat map', {
|
|
252
|
+
user: ctx.auth?.subject,
|
|
253
|
+
offerId: input.offerId
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const seatMaps = await this.duffelService.getSeatsForOffer(input.offerId);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
offerId: input.offerId,
|
|
260
|
+
cabins: seatMaps.map((cabin: any) => ({
|
|
261
|
+
cabinClass: cabin.cabin_class,
|
|
262
|
+
rows: cabin.rows.map((row: any) => ({
|
|
263
|
+
rowNumber: row.row_number,
|
|
264
|
+
seats: row.sections.flatMap((section: any) =>
|
|
265
|
+
section.elements.filter((el: any) => el.type === 'seat').map((seat: any) => ({
|
|
266
|
+
id: seat.id,
|
|
267
|
+
column: seat.designator,
|
|
268
|
+
available: seat.available_services?.length > 0,
|
|
269
|
+
price: seat.available_services?.[0]?.total_amount,
|
|
270
|
+
currency: seat.available_services?.[0]?.total_currency,
|
|
271
|
+
type: seat.disclosures?.join(', ') || 'standard'
|
|
272
|
+
}))
|
|
273
|
+
)
|
|
274
|
+
}))
|
|
275
|
+
})),
|
|
276
|
+
message: 'Select your preferred seats from the available options'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Tool({
|
|
281
|
+
name: 'cancel_order',
|
|
282
|
+
description: 'Cancel a flight order and request refund if applicable',
|
|
283
|
+
inputSchema: z.object({
|
|
284
|
+
orderId: z.string().describe('The order ID to cancel')
|
|
285
|
+
}),
|
|
286
|
+
examples: {
|
|
287
|
+
request: {
|
|
288
|
+
orderId: 'ord_123456'
|
|
289
|
+
},
|
|
290
|
+
response: {
|
|
291
|
+
orderId: 'ord_123456',
|
|
292
|
+
cancellationId: 'ocr_123456',
|
|
293
|
+
status: 'cancelled',
|
|
294
|
+
refundAmount: '450.00',
|
|
295
|
+
refundCurrency: 'USD',
|
|
296
|
+
confirmedAt: '2024-03-01T12:00:00Z',
|
|
297
|
+
message: 'Order cancelled. Refund of USD 450.00 will be processed.'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
@UseGuards(OAuthGuard)
|
|
302
|
+
@Widget('order-cancellation')
|
|
303
|
+
async cancelOrder(input: any, ctx: ExecutionContext) {
|
|
304
|
+
ctx.logger.info('Cancelling order', {
|
|
305
|
+
user: ctx.auth?.subject,
|
|
306
|
+
orderId: input.orderId
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const cancellation = await this.duffelService.cancelOrder(input.orderId);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
orderId: input.orderId,
|
|
313
|
+
cancellationId: cancellation.id,
|
|
314
|
+
status: 'cancelled',
|
|
315
|
+
refundAmount: cancellation.refund_amount,
|
|
316
|
+
refundCurrency: cancellation.refund_currency,
|
|
317
|
+
confirmedAt: cancellation.confirmed_at,
|
|
318
|
+
message: cancellation.refund_amount
|
|
319
|
+
? `Order cancelled. Refund of ${cancellation.refund_currency} ${cancellation.refund_amount} will be processed.`
|
|
320
|
+
: 'Order cancelled. No refund available for this booking.'
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from 'nitrostack';
|
|
2
|
+
import { FlightTools } from './flights.tools.js';
|
|
3
|
+
import { BookingTools } from './booking.tools.js';
|
|
4
|
+
import { FlightPrompts } from './flights.prompts.js';
|
|
5
|
+
import { FlightResources } from './flights.resources.js';
|
|
6
|
+
import { DuffelService } from '../../services/duffel.service.js';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
name: 'flights',
|
|
10
|
+
description: 'Professional flight search and booking system powered by Duffel API',
|
|
11
|
+
controllers: [FlightTools, BookingTools, FlightPrompts, FlightResources],
|
|
12
|
+
providers: [DuffelService]
|
|
13
|
+
})
|
|
14
|
+
export class FlightsModule { }
|