nitrostack 1.0.15 → 1.0.16

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/dev.d.ts.map +1 -1
  3. package/dist/cli/commands/dev.js +2 -1
  4. package/dist/cli/commands/dev.js.map +1 -1
  5. package/dist/cli/mcp-dev-wrapper.js +30 -17
  6. package/dist/cli/mcp-dev-wrapper.js.map +1 -1
  7. package/dist/core/transports/http-server.d.ts.map +1 -1
  8. package/dist/core/transports/http-server.js +21 -1
  9. package/dist/core/transports/http-server.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/studio/app/api/chat/route.ts +12 -1
  12. package/src/studio/app/api/init/route.ts +28 -4
  13. package/src/studio/app/auth/page.tsx +13 -9
  14. package/src/studio/app/chat/page.tsx +544 -133
  15. package/src/studio/app/health/page.tsx +101 -99
  16. package/src/studio/app/layout.tsx +23 -3
  17. package/src/studio/app/page.tsx +61 -56
  18. package/src/studio/app/ping/page.tsx +13 -8
  19. package/src/studio/app/prompts/page.tsx +72 -70
  20. package/src/studio/app/resources/page.tsx +88 -86
  21. package/src/studio/app/settings/page.tsx +270 -0
  22. package/src/studio/components/Sidebar.tsx +197 -35
  23. package/src/studio/lib/http-client-transport.ts +222 -0
  24. package/src/studio/lib/llm-service.ts +97 -0
  25. package/src/studio/lib/log-manager.ts +76 -0
  26. package/src/studio/lib/mcp-client.ts +103 -13
  27. package/src/studio/package-lock.json +3129 -0
  28. package/src/studio/package.json +1 -0
  29. package/templates/typescript-auth/README.md +3 -1
  30. package/templates/typescript-auth/src/db/database.ts +5 -8
  31. package/templates/typescript-auth/src/index.ts +13 -2
  32. package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +49 -6
  33. package/templates/typescript-auth/src/modules/cart/cart.tools.ts +13 -17
  34. package/templates/typescript-auth/src/modules/orders/orders.tools.ts +38 -16
  35. package/templates/typescript-auth/src/modules/products/products.tools.ts +4 -4
  36. package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +25 -0
  37. package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +26 -1
  38. package/templates/typescript-auth-api-key/README.md +3 -1
  39. package/templates/typescript-auth-api-key/src/index.ts +11 -3
  40. package/templates/typescript-starter/README.md +3 -1
@@ -16,6 +16,7 @@
16
16
  "@fontsource/jetbrains-mono": "^5.2.8",
17
17
  "@modelcontextprotocol/sdk": "^1.0.4",
18
18
  "clsx": "^2.1.0",
19
+ "eventsource": "^2.0.2",
19
20
  "lucide-react": "^0.546.0",
20
21
  "next": "^14.2.5",
21
22
  "react": "^18.3.1",
@@ -57,10 +57,12 @@ npm run dev
57
57
  ```
58
58
 
59
59
  This starts:
60
- - **MCP Server** (stdio mode) - Hot reloads on code changes
60
+ - **MCP Server** (dual transport: STDIO + HTTP on port 3002) - Hot reloads on code changes
61
61
  - **Studio** on http://localhost:3000 - Visual testing environment
62
62
  - **Widget Dev Server** on http://localhost:3001 - Hot module replacement
63
63
 
64
+ > 💡 **Dual Transport**: Your server exposes tools via both STDIO (for direct connections) and HTTP (for remote access on port 3002). Switch between transports in Studio → Settings.
65
+
64
66
  The `nitrostack dev` command handles everything automatically:
65
67
  - ✅ Auto-detects widget directory
66
68
  - ✅ Installs dependencies (if needed)
@@ -109,14 +109,13 @@ export function initializeSchema(): void {
109
109
  CREATE TABLE IF NOT EXISTS orders (
110
110
  id TEXT PRIMARY KEY,
111
111
  user_id TEXT NOT NULL,
112
- address_id TEXT NOT NULL,
113
- total_amount REAL NOT NULL,
112
+ shipping_address_id TEXT NOT NULL,
113
+ total REAL NOT NULL,
114
114
  status TEXT NOT NULL DEFAULT 'pending',
115
- payment_method TEXT NOT NULL DEFAULT 'cash_on_delivery',
115
+ payment_method TEXT NOT NULL DEFAULT 'credit_card',
116
116
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
117
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
118
117
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
119
- FOREIGN KEY (address_id) REFERENCES addresses(id)
118
+ FOREIGN KEY (shipping_address_id) REFERENCES addresses(id)
120
119
  )
121
120
  `);
122
121
 
@@ -126,10 +125,8 @@ export function initializeSchema(): void {
126
125
  id TEXT PRIMARY KEY,
127
126
  order_id TEXT NOT NULL,
128
127
  product_id TEXT NOT NULL,
129
- product_name TEXT NOT NULL,
130
- product_price REAL NOT NULL,
131
128
  quantity INTEGER NOT NULL,
132
- subtotal REAL NOT NULL,
129
+ price REAL NOT NULL,
133
130
  FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
134
131
  FOREIGN KEY (product_id) REFERENCES products(id)
135
132
  )
@@ -15,8 +15,19 @@ async function bootstrap() {
15
15
  // Create and start the MCP server from AppModule
16
16
  const server = await McpApplicationFactory.create(AppModule);
17
17
 
18
- // Start the server
19
- await server.start();
18
+ // Start the server with dual transport (STDIO + HTTP)
19
+ // This allows both direct stdio connections and HTTP-based connections
20
+ // Note: Studio runs on 3000, Widgets on 3001, so MCP HTTP uses 3002
21
+ const port = parseInt(process.env.PORT || '3002');
22
+ await server.start('dual', {
23
+ port,
24
+ host: '0.0.0.0',
25
+ basePath: '/mcp',
26
+ });
27
+
28
+ console.error(`\n🚀 Server started successfully (DUAL: STDIO + HTTP)`);
29
+ console.error(`📡 MCP Protocol: STDIO (for direct connections)`);
30
+ console.error(`🌐 HTTP Server: http://0.0.0.0:${port}/mcp`);
20
31
  }
21
32
 
22
33
  // Start the application
@@ -13,14 +13,14 @@ export class AddressesTools {
13
13
  */
14
14
  @Tool({
15
15
  name: 'add_address',
16
- description: 'Add a new shipping address. Requires authentication.',
16
+ description: 'Add a new shipping address. Requires authentication. SMART TIP: If user mentions a well-known city, infer the state and country (e.g., Bangalore → Karnataka, India; New York → NY, USA; London → England, UK).',
17
17
  inputSchema: z.object({
18
18
  full_name: z.string().describe('Full name for delivery'),
19
19
  street: z.string().describe('Street address'),
20
- city: z.string().describe('City'),
21
- state: z.string().describe('State/Province'),
22
- zip_code: z.string().describe('ZIP/Postal code'),
23
- country: z.string().default('USA').describe('Country'),
20
+ city: z.string().describe('City name (e.g., Bangalore, San Francisco, London)'),
21
+ state: z.string().optional().describe('State/Province (e.g., Karnataka, CA, England). Can be inferred from city in many cases.'),
22
+ zip_code: z.string().optional().describe('ZIP/Postal code (optional, can be left empty)'),
23
+ country: z.string().default('USA').describe('Country (e.g., India, USA, UK). Can be inferred from well-known cities.'),
24
24
  phone: z.string().describe('Contact phone number'),
25
25
  is_default: z.boolean().default(false).describe('Set as default address'),
26
26
  }),
@@ -49,6 +49,44 @@ export class AddressesTools {
49
49
  const userId = context.auth?.subject;
50
50
  const addressId = uuidv4();
51
51
 
52
+ // Smart defaults for common locations
53
+ const cityLower = input.city?.toLowerCase() || '';
54
+
55
+ // If state not provided, try to infer from well-known cities
56
+ if (!input.state) {
57
+ const cityToState: Record<string, { state: string; country: string }> = {
58
+ 'bangalore': { state: 'Karnataka', country: 'India' },
59
+ 'bengaluru': { state: 'Karnataka', country: 'India' },
60
+ 'mumbai': { state: 'Maharashtra', country: 'India' },
61
+ 'delhi': { state: 'Delhi', country: 'India' },
62
+ 'hyderabad': { state: 'Telangana', country: 'India' },
63
+ 'chennai': { state: 'Tamil Nadu', country: 'India' },
64
+ 'pune': { state: 'Maharashtra', country: 'India' },
65
+ 'kolkata': { state: 'West Bengal', country: 'India' },
66
+ 'san francisco': { state: 'CA', country: 'USA' },
67
+ 'new york': { state: 'NY', country: 'USA' },
68
+ 'los angeles': { state: 'CA', country: 'USA' },
69
+ 'chicago': { state: 'IL', country: 'USA' },
70
+ 'boston': { state: 'MA', country: 'USA' },
71
+ 'seattle': { state: 'WA', country: 'USA' },
72
+ 'london': { state: 'England', country: 'UK' },
73
+ 'manchester': { state: 'England', country: 'UK' },
74
+ 'toronto': { state: 'Ontario', country: 'Canada' },
75
+ 'vancouver': { state: 'British Columbia', country: 'Canada' },
76
+ };
77
+
78
+ const inferred = cityToState[cityLower];
79
+ if (inferred) {
80
+ input.state = inferred.state;
81
+ input.country = inferred.country;
82
+ context.logger.info(`Smart inference: ${input.city} → ${input.state}, ${input.country}`);
83
+ }
84
+ }
85
+
86
+ // Set defaults if still missing
87
+ input.state = input.state || 'N/A';
88
+ input.zip_code = input.zip_code || '000000';
89
+
52
90
  // If this is set as default, unset other defaults
53
91
  if (input.is_default) {
54
92
  db.prepare(`
@@ -72,12 +110,17 @@ export class AddressesTools {
72
110
  input.is_default ? 1 : 0
73
111
  );
74
112
 
75
- context.logger.info(`Address added: ${input.city}, ${input.state}`);
113
+ context.logger.info(`Address added: ${input.city}, ${input.state}, ${input.country}`);
76
114
 
77
115
  return {
78
116
  message: 'Address added successfully!',
79
117
  addressId,
80
118
  isDefault: input.is_default,
119
+ address: {
120
+ city: input.city,
121
+ state: input.state,
122
+ country: input.country,
123
+ },
81
124
  };
82
125
  }
83
126
 
@@ -13,9 +13,9 @@ export class CartTools {
13
13
  */
14
14
  @Tool({
15
15
  name: 'add_to_cart',
16
- description: 'Add a product to the shopping cart. Requires authentication.',
16
+ description: 'Add a product to the shopping cart. Requires authentication. IMPORTANT: Use the product ID from browse_products or get_product_details results. If user says "add apple", find the product with name "Apple" from previous results and use its ID.',
17
17
  inputSchema: z.object({
18
- product_id: z.string().describe('The ID of the product to add'),
18
+ product_id: z.string().describe('The ID of the product to add (e.g., "prod-3"). Get this from browse_products results by matching the product name.'),
19
19
  quantity: z.number().min(1).default(1).describe('Quantity to add'),
20
20
  }),
21
21
  examples: {
@@ -77,13 +77,11 @@ export class CartTools {
77
77
 
78
78
  return {
79
79
  message: `Added to cart!`,
80
- item: {
81
- id: input.product_id,
82
- name: product.name,
83
- price: product.price,
84
- quantity: input.quantity,
85
- image_url: product.image_url,
86
- },
80
+ product_id: input.product_id,
81
+ product_name: product.name,
82
+ price: product.price,
83
+ quantity: input.quantity,
84
+ image_url: product.image_url,
87
85
  cartItemCount: cartCount.total || 0,
88
86
  };
89
87
  } else {
@@ -104,13 +102,11 @@ export class CartTools {
104
102
 
105
103
  return {
106
104
  message: `Added to cart!`,
107
- item: {
108
- id: input.product_id,
109
- name: product.name,
110
- price: product.price,
111
- quantity: input.quantity,
112
- image_url: product.image_url,
113
- },
105
+ product_id: input.product_id,
106
+ product_name: product.name,
107
+ price: product.price,
108
+ quantity: input.quantity,
109
+ image_url: product.image_url,
114
110
  cartItemCount: cartCount.total || 0,
115
111
  };
116
112
  }
@@ -156,7 +152,7 @@ export class CartTools {
156
152
  const userId = context.auth?.subject;
157
153
 
158
154
  const items: any[] = db.prepare(`
159
- SELECT c.id, c.quantity, p.id as product_id, p.name, p.price, p.image_url
155
+ SELECT c.id, c.quantity, p.id as product_id, p.name, p.price, p.image_url, p.description, p.stock
160
156
  FROM carts c
161
157
  JOIN products p ON c.product_id = p.id
162
158
  WHERE c.user_id = ?
@@ -13,10 +13,10 @@ export class OrdersTools {
13
13
  */
14
14
  @Tool({
15
15
  name: 'create_order',
16
- description: 'Create an order from the current shopping cart. Requires authentication and at least one item in cart.',
16
+ description: 'Create an order from the current shopping cart. Requires authentication and at least one item in cart. A default shipping address will be automatically created if the user doesn\'t have one.',
17
17
  inputSchema: z.object({
18
18
  payment_method: z.enum(['credit_card', 'paypal', 'apple_pay']).describe('Payment method'),
19
- shipping_address_id: z.string().optional().describe('Address ID for shipping. Uses default if not provided.'),
19
+ shipping_address_id: z.string().optional().describe('Optional: Address ID for shipping. If not provided, uses or creates a default address automatically.'),
20
20
  }),
21
21
  examples: {
22
22
  request: {
@@ -57,16 +57,21 @@ export class OrdersTools {
57
57
  @Widget('order-confirmation')
58
58
  @UseGuards(JWTGuard)
59
59
  async createOrder(input: any, context: ExecutionContext) {
60
- const db = getDatabase();
61
- const userId = context.auth?.subject;
62
-
63
- // Get cart items
64
- const cartItems: any[] = db.prepare(`
65
- SELECT c.*, p.name, p.price, p.stock, p.image_url
66
- FROM carts c
67
- JOIN products p ON c.product_id = p.id
68
- WHERE c.user_id = ?
69
- `).all(userId);
60
+ try {
61
+ const db = getDatabase();
62
+ const userId = context.auth?.subject;
63
+
64
+ context.logger.info(`Creating order for user: ${userId}, payment_method: ${input.payment_method}`);
65
+
66
+ // Get cart items
67
+ const cartItems: any[] = db.prepare(`
68
+ SELECT c.*, p.name, p.price, p.stock, p.image_url
69
+ FROM carts c
70
+ JOIN products p ON c.product_id = p.id
71
+ WHERE c.user_id = ?
72
+ `).all(userId);
73
+
74
+ context.logger.info(`Found ${cartItems.length} items in cart`);
70
75
 
71
76
  if (cartItems.length === 0) {
72
77
  throw new Error('Cart is empty. Add items to cart before creating an order.');
@@ -79,7 +84,7 @@ export class OrdersTools {
79
84
  }
80
85
  }
81
86
 
82
- // Get shipping address
87
+ // Get shipping address (create a temporary one if none exists)
83
88
  let addressId = input.shipping_address_id;
84
89
  if (!addressId) {
85
90
  const defaultAddress: any = db.prepare(`
@@ -87,10 +92,18 @@ export class OrdersTools {
87
92
  `).get(userId);
88
93
 
89
94
  if (!defaultAddress) {
90
- throw new Error('No shipping address found. Please add an address first.');
95
+ // Create a temporary default address for the user
96
+ const tempAddressId = uuidv4();
97
+ db.prepare(`
98
+ INSERT INTO addresses (id, user_id, full_name, street, city, state, zip_code, country, phone, is_default)
99
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
100
+ `).run(tempAddressId, userId, 'Default User', '123 Main St', 'City', 'State', '00000', 'USA', '000-000-0000', 1);
101
+
102
+ context.logger.info('Created temporary default address for user');
103
+ addressId = tempAddressId;
104
+ } else {
105
+ addressId = defaultAddress.id;
91
106
  }
92
-
93
- addressId = defaultAddress.id;
94
107
  }
95
108
 
96
109
  // Calculate total
@@ -143,6 +156,10 @@ export class OrdersTools {
143
156
  })),
144
157
  },
145
158
  };
159
+ } catch (error: any) {
160
+ context.logger.error(`Order creation failed: ${error.message}`, { error, stack: error.stack });
161
+ throw new Error(`Failed to create order: ${error.message}`);
162
+ }
146
163
  }
147
164
 
148
165
  /**
@@ -200,6 +217,11 @@ export class OrdersTools {
200
217
  return {
201
218
  orders,
202
219
  totalOrders: orders.length,
220
+ pagination: {
221
+ page: 1,
222
+ totalPages: 1,
223
+ totalResults: orders.length,
224
+ },
203
225
  };
204
226
  }
205
227
 
@@ -39,10 +39,10 @@ export class ProductsTools {
39
39
  */
40
40
  @Tool({
41
41
  name: 'browse_products',
42
- description: 'Browse available products. Can filter by category, search by name, and paginate results.',
42
+ description: 'Browse available products. Can filter by category, search by name, and paginate results. Returns products with their IDs which can be used with other tools like add_to_cart.',
43
43
  inputSchema: z.object({
44
44
  category: z.enum(['Electronics', 'Clothing', 'Home', 'Books', 'Sports', 'All']).default('All').describe('Filter by product category'),
45
- search: z.string().optional().describe('Search products by name'),
45
+ search: z.string().optional().describe('Search products by name (e.g., "apple" will find products containing "apple")'),
46
46
  page: z.number().min(1).default(1).describe('Page number for pagination'),
47
47
  limit: z.number().min(1).max(50).default(10).describe('Number of products per page'),
48
48
  }),
@@ -174,9 +174,9 @@ export class ProductsTools {
174
174
  */
175
175
  @Tool({
176
176
  name: 'get_product_details',
177
- description: 'Get detailed information about a specific product by ID.',
177
+ description: 'Get detailed information about a specific product by ID. Use this if you need more details about a product from browse_products results.',
178
178
  inputSchema: z.object({
179
- product_id: z.string().describe('The ID of the product to retrieve'),
179
+ product_id: z.string().describe('The ID of the product to retrieve (e.g., "prod-3" from browse_products results)'),
180
180
  }),
181
181
  examples: {
182
182
  request: {
@@ -5,8 +5,33 @@ import { OrderConfirmationData } from '../../types/tool-data';
5
5
  import * as styles from '../../styles/ecommerce';
6
6
 
7
7
  function OrderConfirmationWidget({ data }: { data: OrderConfirmationData }) {
8
+ // Handle missing or undefined data
9
+ if (!data) {
10
+ return (
11
+ <div style={styles.containerStyle}>
12
+ <div style={styles.cardStyle}>
13
+ <p style={{ color: styles.colors.error }}>No order data available</p>
14
+ </div>
15
+ </div>
16
+ );
17
+ }
18
+
8
19
  const { order, message } = data;
9
20
 
21
+ // Handle missing order
22
+ if (!order || !order.id) {
23
+ return (
24
+ <div style={styles.containerStyle}>
25
+ <div style={styles.cardStyle}>
26
+ <p style={{ color: styles.colors.error }}>Order data incomplete</p>
27
+ <pre style={{ fontSize: '12px', marginTop: '10px' }}>
28
+ {JSON.stringify(data, null, 2)}
29
+ </pre>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
10
35
  return (
11
36
  <div style={styles.containerStyle}>
12
37
  <div style={{
@@ -5,8 +5,33 @@ import { ProductsGridData } from '../../types/tool-data';
5
5
  import * as styles from '../../styles/ecommerce';
6
6
 
7
7
  function ProductsGridWidget({ data }: { data: ProductsGridData }) {
8
+ // Handle missing or undefined data
9
+ if (!data) {
10
+ return (
11
+ <div style={styles.containerStyle}>
12
+ <div style={styles.cardStyle}>
13
+ <p style={{ color: styles.colors.error }}>No data available</p>
14
+ </div>
15
+ </div>
16
+ );
17
+ }
18
+
8
19
  const { products, pagination } = data;
9
20
 
21
+ // Handle missing products array
22
+ if (!products || !Array.isArray(products)) {
23
+ return (
24
+ <div style={styles.containerStyle}>
25
+ <div style={styles.cardStyle}>
26
+ <p style={{ color: styles.colors.error }}>Invalid product data</p>
27
+ <pre style={{ fontSize: '12px', marginTop: '10px' }}>
28
+ {JSON.stringify(data, null, 2)}
29
+ </pre>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
10
35
  if (products.length === 0) {
11
36
  return (
12
37
  <div style={styles.containerStyle}>
@@ -152,7 +177,7 @@ function ProductsGridWidget({ data }: { data: ProductsGridData }) {
152
177
  ))}
153
178
  </div>
154
179
 
155
- {pagination.totalPages > 1 && (
180
+ {pagination && pagination.totalPages > 1 && (
156
181
  <div style={{
157
182
  marginTop: styles.spacing['2xl'],
158
183
  textAlign: 'center',
@@ -43,7 +43,9 @@ npm run build
43
43
  nitrostack dev
44
44
  ```
45
45
 
46
- The server will start with API key authentication enabled.
46
+ The server will start with API key authentication enabled in dual transport mode (STDIO + HTTP on port 3002).
47
+
48
+ > 💡 **Dual Transport**: Your server exposes tools via both STDIO (for direct connections) and HTTP (for remote access on port 3002). Switch between transports in Studio → Settings.
47
49
 
48
50
  ### 2. Open NitroStack Studio
49
51
 
@@ -30,10 +30,18 @@ async function bootstrap() {
30
30
  console.error(' - Protected tools require valid API key');
31
31
  console.error(' - Public tools accessible without authentication\n');
32
32
 
33
- // Start the MCP server
34
- await app.start();
33
+ // Start the MCP server with dual transport (STDIO + HTTP)
34
+ // Note: Studio runs on 3000, Widgets on 3001, so MCP HTTP uses 3002
35
+ const port = parseInt(process.env.PORT || '3002');
36
+ await app.start('dual', {
37
+ port,
38
+ host: '0.0.0.0',
39
+ basePath: '/mcp',
40
+ });
35
41
 
36
- console.error('🚀 Server started successfully!');
42
+ console.error('🚀 Server started successfully (DUAL: STDIO + HTTP)!');
43
+ console.error(`📡 MCP Protocol: STDIO (for direct connections)`);
44
+ console.error(`🌐 HTTP Server: http://0.0.0.0:${port}/mcp`);
37
45
  console.error(' Open NitroStack Studio to test the server');
38
46
  console.error(' Set your API key in Studio → Auth → API Key section\n');
39
47
 
@@ -46,10 +46,12 @@ npm run dev
46
46
  ```
47
47
 
48
48
  This starts:
49
- - **MCP Server** (stdio mode) - Hot reloads on code changes
49
+ - **MCP Server** (dual transport: STDIO + HTTP on port 3002) - Hot reloads on code changes
50
50
  - **Studio** on http://localhost:3000 - Visual testing environment
51
51
  - **Widget Dev Server** on http://localhost:3001 - Hot module replacement
52
52
 
53
+ > 💡 **Dual Transport**: Your server exposes tools via both STDIO (for direct connections) and HTTP (for remote access on port 3002). Switch between transports in Studio → Settings.
54
+
53
55
  The `nitrostack dev` command handles everything automatically:
54
56
  - ✅ Auto-detects widget directory
55
57
  - ✅ Installs dependencies (if needed)