sunpeak 0.15.4 → 0.16.2

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 (105) hide show
  1. package/README.md +53 -49
  2. package/bin/commands/build.mjs +119 -6
  3. package/bin/commands/dev.mjs +168 -27
  4. package/bin/commands/new.mjs +13 -3
  5. package/bin/commands/start.mjs +215 -0
  6. package/bin/lib/extract-resource.mjs +1 -1
  7. package/bin/lib/extract-tool.mjs +78 -0
  8. package/bin/lib/patterns.mjs +2 -26
  9. package/bin/sunpeak.js +11 -1
  10. package/dist/chatgpt/index.cjs +3 -6
  11. package/dist/chatgpt/index.cjs.map +1 -1
  12. package/dist/chatgpt/index.d.ts +1 -1
  13. package/dist/chatgpt/index.js +6 -9
  14. package/dist/claude/index.cjs +1 -1
  15. package/dist/claude/index.js +1 -1
  16. package/dist/discovery-CH80W5l9.js +217 -0
  17. package/dist/discovery-CH80W5l9.js.map +1 -0
  18. package/dist/discovery-DmB8_4QL.cjs +216 -0
  19. package/dist/discovery-DmB8_4QL.cjs.map +1 -0
  20. package/dist/{index-Cngntkp2.cjs → index-Bll1bszc.cjs} +3 -6
  21. package/dist/{index-Cngntkp2.cjs.map → index-Bll1bszc.cjs.map} +1 -1
  22. package/dist/{index-Ce_5ZIdJ.js → index-CACtnwu2.js} +3 -6
  23. package/dist/{index-Ce_5ZIdJ.js.map → index-CACtnwu2.js.map} +1 -1
  24. package/dist/{index-CutQgPzR.js → index-CLcr8IyR.js} +3 -6
  25. package/dist/index-CLcr8IyR.js.map +1 -0
  26. package/dist/{index-B0dxRJvS.cjs → index-CaQmwZJc.cjs} +3 -6
  27. package/dist/index-CaQmwZJc.cjs.map +1 -0
  28. package/dist/index.cjs +49 -6
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +3405 -3362
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/discovery-cli.cjs +58 -5
  34. package/dist/lib/discovery-cli.cjs.map +1 -1
  35. package/dist/lib/discovery-cli.d.ts +3 -2
  36. package/dist/lib/discovery-cli.js +61 -8
  37. package/dist/lib/discovery-cli.js.map +1 -1
  38. package/dist/lib/discovery.d.ts +42 -43
  39. package/dist/lib/extract-tool.d.ts +12 -0
  40. package/dist/mcp/favicon.d.ts +1 -1
  41. package/dist/mcp/index.cjs +1582 -5
  42. package/dist/mcp/index.cjs.map +1 -1
  43. package/dist/mcp/index.d.ts +3 -1
  44. package/dist/mcp/index.js +1583 -6
  45. package/dist/mcp/index.js.map +1 -1
  46. package/dist/mcp/production-server.d.ts +156 -0
  47. package/dist/mcp/types.d.ts +24 -1
  48. package/dist/platform/chatgpt/index.cjs +1 -1
  49. package/dist/platform/chatgpt/index.js +1 -1
  50. package/dist/{protocol-DFbsCx7E.js → protocol-BD5jDQEx.js} +8 -1
  51. package/dist/{protocol-DFbsCx7E.js.map → protocol-BD5jDQEx.js.map} +1 -1
  52. package/dist/{protocol-CL4_Npj5.cjs → protocol-BOjXuK6l.cjs} +8 -1
  53. package/dist/{protocol-CL4_Npj5.cjs.map → protocol-BOjXuK6l.cjs.map} +1 -1
  54. package/dist/simulator/index.cjs +2 -5
  55. package/dist/simulator/index.cjs.map +1 -1
  56. package/dist/simulator/index.d.ts +1 -1
  57. package/dist/simulator/index.js +5 -8
  58. package/dist/simulator/simulator-url.d.ts +9 -9
  59. package/dist/{simulator-CxrtnguM.js → simulator-B7rw83zP.js} +9 -3
  60. package/dist/{simulator-CxrtnguM.js.map → simulator-B7rw83zP.js.map} +1 -1
  61. package/dist/{simulator-DcfQBRXE.cjs → simulator-DjZNa1MI.cjs} +9 -3
  62. package/dist/{simulator-DcfQBRXE.cjs.map → simulator-DjZNa1MI.cjs.map} +1 -1
  63. package/dist/simulator-url-CuLqtnSS.js.map +1 -1
  64. package/dist/simulator-url-rgg_KYOg.cjs.map +1 -1
  65. package/dist/types/resource-config.d.ts +7 -5
  66. package/dist/{use-app-BnoSPiUT.cjs → use-app-BpAJqzdE.cjs} +50 -21
  67. package/dist/{use-app-BnoSPiUT.cjs.map → use-app-BpAJqzdE.cjs.map} +1 -1
  68. package/dist/{use-app-D_TeaMFG.js → use-app-WOUdh1PR.js} +52 -23
  69. package/dist/{use-app-D_TeaMFG.js.map → use-app-WOUdh1PR.js.map} +1 -1
  70. package/package.json +1 -1
  71. package/template/.sunpeak/dev.tsx +8 -4
  72. package/template/.sunpeak/resource-loader.tsx +2 -1
  73. package/template/README.md +27 -22
  74. package/template/package.json +3 -1
  75. package/template/src/resources/albums/{albums-resource.test.tsx → albums.test.tsx} +1 -1
  76. package/template/src/resources/albums/{albums-resource.tsx → albums.tsx} +0 -1
  77. package/template/src/resources/carousel/{carousel-resource.test.tsx → carousel.test.tsx} +1 -1
  78. package/template/src/resources/carousel/{carousel-resource.tsx → carousel.tsx} +0 -1
  79. package/template/src/resources/index.ts +4 -4
  80. package/template/src/resources/map/{map-resource.test.tsx → map.test.tsx} +1 -1
  81. package/template/src/resources/map/{map-resource.tsx → map.tsx} +0 -1
  82. package/template/src/resources/review/{review-resource.test.tsx → review.test.tsx} +1 -1
  83. package/template/src/resources/review/{review-resource.tsx → review.tsx} +1 -2
  84. package/template/src/server.ts +17 -0
  85. package/template/src/tools/review-diff.ts +24 -0
  86. package/template/src/tools/review-post.ts +26 -0
  87. package/template/src/tools/review-purchase.ts +31 -0
  88. package/template/src/tools/show-albums.ts +22 -0
  89. package/template/src/tools/show-carousel.ts +25 -0
  90. package/template/src/tools/show-map.ts +29 -0
  91. package/template/tests/e2e/albums.spec.ts +6 -6
  92. package/template/tests/e2e/carousel.spec.ts +6 -6
  93. package/template/tests/e2e/map.spec.ts +11 -11
  94. package/template/tests/simulations/{review/review-diff-simulation.json → review-diff.json} +1 -31
  95. package/template/tests/simulations/{review/review-post-simulation.json → review-post.json} +1 -37
  96. package/template/tests/simulations/{review/review-purchase-simulation.json → review-purchase.json} +1 -38
  97. package/template/tests/simulations/{albums/albums-show-simulation.json → show-albums.json} +1 -24
  98. package/template/tests/simulations/{carousel/carousel-show-simulation.json → show-carousel.json} +1 -24
  99. package/template/tests/simulations/{map/map-show-simulation.json → show-map.json} +1 -35
  100. package/dist/discovery-CRR3SlyI.cjs +0 -156
  101. package/dist/discovery-CRR3SlyI.cjs.map +0 -1
  102. package/dist/discovery-DzV3HLXs.js +0 -157
  103. package/dist/discovery-DzV3HLXs.js.map +0 -1
  104. package/dist/index-B0dxRJvS.cjs.map +0 -1
  105. package/dist/index-CutQgPzR.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.15.4",
3
+ "version": "0.16.2",
4
4
  "description": "Local-first MCP Apps framework. Quickstart, build, test, and ship your Claude or ChatGPT App!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -4,8 +4,8 @@
4
4
  * This file bootstraps the multi-host simulator for development.
5
5
  *
6
6
  * Auto-discovers simulations and resources by file naming convention:
7
- * - tests/simulations/{resource}/{resource}-{scenario}-simulation.json
8
- * - src/resources/{resource}/{resource}-resource.tsx (component + resource metadata)
7
+ * - tests/simulations/*.json
8
+ * - src/resources/{resource}/{resource}.tsx (component + resource metadata)
9
9
  * - src/resources/{resource}/{Resource}Resource component (PascalCase)
10
10
  */
11
11
  import { StrictMode } from 'react';
@@ -18,9 +18,13 @@ const { Simulator, buildDevSimulations } = simulator;
18
18
 
19
19
  // Build simulations from discovered files
20
20
  const simulations = buildDevSimulations({
21
- simulationModules: import.meta.glob('../tests/simulations/*/*-simulation.json', { eager: true }),
22
- resourceModules: import.meta.glob('../src/resources/*/*-resource.tsx', { eager: true }),
21
+ simulationModules: import.meta.glob('../tests/simulations/*.json', { eager: true }),
23
22
  resourceComponents: resourceComponents as Record<string, React.ComponentType>,
23
+ toolModules: import.meta.glob('../src/tools/*.ts', { eager: true }),
24
+ resourceModules: import.meta.glob(
25
+ ['../src/resources/*/*.tsx', '!../src/resources/*/*.test.tsx'],
26
+ { eager: true }
27
+ ),
24
28
  });
25
29
 
26
30
  // Read app config from environment or use defaults
@@ -17,6 +17,7 @@
17
17
  import { StrictMode } from 'react';
18
18
  import { createRoot } from 'react-dom/client';
19
19
  import { AppProvider } from 'sunpeak';
20
+ import pkg from '../package.json';
20
21
  import '../src/styles/globals.css';
21
22
  import resourceComponents from '../src/resources';
22
23
 
@@ -39,7 +40,7 @@ if (!componentName) {
39
40
 
40
41
  root.render(
41
42
  <StrictMode>
42
- <AppProvider appInfo={{ name: componentName, version: '1.0.0' }}>
43
+ <AppProvider appInfo={{ name: pkg.name || componentName, version: pkg.version || '1.0.0' }}>
43
44
  <Component />
44
45
  </AppProvider>
45
46
  </StrictMode>
@@ -18,7 +18,8 @@ That's it! Edit the resource files in [./src/resources/](./src/resources/) to bu
18
18
  pnpm test # Run tests with Vitest.
19
19
  pnpm test:e2e # Run end-to-end tests with Playwright.
20
20
  sunpeak dev # Start dev server + MCP endpoint.
21
- sunpeak build # Build all resources for production.
21
+ sunpeak build # Build resources and compile tools for production.
22
+ sunpeak start # Start the production MCP server.
22
23
  sunpeak upgrade # Upgrade sunpeak to latest version.
23
24
  ```
24
25
 
@@ -32,11 +33,15 @@ Using a Review page as an example, sunpeak projects look like:
32
33
  my-app/
33
34
  ├── src/resources/
34
35
  │ └── review/
35
- │ └── review-resource.tsx # Review UI component + resource metadata.
36
+ │ └── review.tsx # Review UI component + resource metadata.
37
+ ├── src/tools/
38
+ │ ├── review-diff.ts # Tool: metadata, schema, handler.
39
+ │ ├── review-post.ts
40
+ │ └── review-purchase.ts
36
41
  ├── tests/simulations/
37
- └── review/
38
- ├── review-{scenario1}-simulation.json # Mock state for testing.
39
- └── review-{scenario2}-simulation.json # Mock state for testing.
42
+ ├── review-diff.json # Mock state for testing.
43
+ ├── review-post.json
44
+ └── review-purchase.json
40
45
  └── package.json
41
46
  ```
42
47
 
@@ -58,31 +63,31 @@ Once your app is connected, send the name of the app and a tool, like `/sunpeak
58
63
 
59
64
  ## Build & Deploy
60
65
 
61
- Build your app for production:
66
+ Build and start your app for production:
62
67
 
63
68
  ```bash
64
- sunpeak build
69
+ sunpeak build && sunpeak start
65
70
  ```
66
71
 
67
- This creates optimized builds in `dist/`, organized by resource:
72
+ `sunpeak build` creates optimized builds in `dist/`:
68
73
 
69
74
  ```bash
70
75
  dist/
71
76
  ├── albums/
72
77
  │ ├── albums.html # Built resource bundle.
73
78
  │ └── albums.json # ResourceConfig (extracted from .tsx).
74
- ├── review/
75
- │ ├── review.html
76
- │ └── review.json
79
+ ├── tools/
80
+ │ ├── show-albums.js # Compiled tool handler + schema.
81
+ │ └── ...
82
+ ├── server.js # Compiled server entry (if src/server.ts exists).
77
83
  └── ...
78
84
  ```
79
85
 
80
- Each resource folder contains:
81
-
82
- - **`.html` file**: Self-contained bundle with JS and CSS inlined
83
- - **`.json` file**: Resource metadata (extracted from the `resource` export in your `.tsx` file) with a generated `uri` for cache-busting
86
+ `sunpeak start` loads the compiled tools and resources, then starts a production MCP server with real handlers, Zod input validation, and optional auth.
84
87
 
85
- Host these files and reference them as resources in your production MCP server.
88
+ ```bash
89
+ sunpeak start --port 3000 # Custom port (default: 8000)
90
+ ```
86
91
 
87
92
  ## Add a new UI (Resource)
88
93
 
@@ -90,14 +95,14 @@ To add a new UI (MCP Resource), create a new directory under `src/resources/` wi
90
95
 
91
96
  ```
92
97
  src/resources/NAME/
93
- ├── NAME-resource.tsx # React component + resource metadata (required)
94
- ├── NAME-resource.test.tsx # Unit tests (optional)
95
- └── components/ # UI components (optional)
98
+ ├── NAME.tsx # React component + resource metadata (required)
99
+ ├── NAME.test.tsx # Unit tests (optional)
100
+ └── components/ # UI components (optional)
96
101
  ```
97
102
 
98
- Only the resource file (`.tsx`) is required to generate a production build and ship a UI. It must export a `resource` object (`ResourceConfig`) describing the resource metadata, and a React component that renders the UI.
103
+ Only the resource file (`.tsx`) is required to generate a production build and ship a UI. It must export a `resource` object (`ResourceConfig`) describing the resource metadata, and a React component that renders the UI. The resource name is auto-derived from the directory name.
99
104
 
100
- Create the simulation file(s) in `tests/simulations/` if you want to preview your resource in `sunpeak dev`.
105
+ Then create a tool file in `src/tools/` and simulation file(s) in `tests/simulations/` to preview your resource in `sunpeak dev`.
101
106
 
102
107
  ## Coding Agent Skill
103
108
 
@@ -110,6 +115,6 @@ npx skills add Sunpeak-AI/sunpeak@create-sunpeak-app
110
115
  ## Resources
111
116
 
112
117
  - [sunpeak](https://github.com/Sunpeak-AI/sunpeak)
118
+ - [MCP Apps Documentation](https://sunpeak.ai/docs/mcp-apps/introduction)
113
119
  - [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
114
120
  - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
115
- - [ChatGPT Apps SDK UI Documentation](https://developers.openai.com/apps-sdk/build/chatgpt-ui)
@@ -6,6 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "sunpeak dev",
8
8
  "build": "sunpeak build",
9
+ "start": "sunpeak start",
9
10
  "test": "vitest run",
10
11
  "test:e2e": "playwright test"
11
12
  },
@@ -15,7 +16,8 @@
15
16
  "embla-carousel-wheel-gestures": "^8.1.0",
16
17
  "mapbox-gl": "^3.17.0",
17
18
  "sunpeak": "workspace:*",
18
- "tailwind-merge": "^3.4.0"
19
+ "tailwind-merge": "^3.4.0",
20
+ "zod": "^3.25.0"
19
21
  },
20
22
  "devDependencies": {
21
23
  "@playwright/test": "^1.57.0",
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { AlbumsResource } from './albums-resource';
3
+ import { AlbumsResource } from './albums';
4
4
 
5
5
  // Mock sunpeak — SafeArea renders as a plain div
6
6
  vi.mock('sunpeak', () => ({
@@ -3,7 +3,6 @@ import type { ResourceConfig } from 'sunpeak';
3
3
  import { Albums } from './components/albums';
4
4
 
5
5
  export const resource: ResourceConfig = {
6
- name: 'albums',
7
6
  title: 'Albums',
8
7
  description: 'Show photo albums widget',
9
8
  mimeType: 'text/html;profile=mcp-app',
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { CarouselResource } from './carousel-resource';
3
+ import { CarouselResource } from './carousel';
4
4
 
5
5
  // Mock sunpeak hooks
6
6
  interface Place {
@@ -3,7 +3,6 @@ import type { ResourceConfig } from 'sunpeak';
3
3
  import { Carousel, Card } from './components';
4
4
 
5
5
  export const resource: ResourceConfig = {
6
- name: 'carousel',
7
6
  title: 'Carousel',
8
7
  description: 'Show popular places to visit widget',
9
8
  mimeType: 'text/html;profile=mcp-app',
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Auto-discovers and re-exports all resource components.
3
3
  *
4
- * Discovers all {resource}/{resource}-resource.tsx files and exports their component
5
- * with a PascalCase name (e.g., albums/albums-resource.tsx -> AlbumsResource).
4
+ * Discovers all {resource}/{resource}.tsx files and exports their component
5
+ * with a PascalCase name (e.g., albums/albums.tsx -> AlbumsResource).
6
6
  *
7
7
  * Supports both export styles:
8
8
  * - Default export: export default MyComponent
@@ -10,8 +10,8 @@
10
10
  */
11
11
  import { createResourceExports } from 'sunpeak';
12
12
 
13
- // Auto-discover all resource component files in subdirectories
14
- const resourceModules = import.meta.glob('./*/*-resource.tsx', { eager: true });
13
+ // Auto-discover all resource component files in subdirectories (exclude test files)
14
+ const resourceModules = import.meta.glob(['./*/*.tsx', '!./*/*.test.tsx'], { eager: true });
15
15
 
16
16
  // Build exports object from discovered files using library helper
17
17
  export default createResourceExports(resourceModules);
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { MapResource } from './map-resource';
3
+ import { MapResource } from './map';
4
4
 
5
5
  // Mock sunpeak — SafeArea renders as a plain div
6
6
  vi.mock('sunpeak', () => ({
@@ -3,7 +3,6 @@ import type { ResourceConfig } from 'sunpeak';
3
3
  import { Map } from './components/map';
4
4
 
5
5
  export const resource: ResourceConfig = {
6
- name: 'map',
7
6
  title: 'Map',
8
7
  description: 'Pizza restaurant finder widget',
9
8
  mimeType: 'text/html;profile=mcp-app',
@@ -1,6 +1,6 @@
1
1
  import { render, screen, fireEvent } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { ReviewResource } from './review-resource';
3
+ import { ReviewResource } from './review';
4
4
 
5
5
  // Mock sunpeak hooks
6
6
  const mockSetState = vi.fn();
@@ -11,7 +11,6 @@ import { Button } from '../../components/button';
11
11
  import { ExpandLg } from '../../components/icon';
12
12
 
13
13
  export const resource: ResourceConfig = {
14
- name: 'review',
15
14
  title: 'Review',
16
15
  description: 'Visualize and review a proposed set of changes or actions',
17
16
  mimeType: 'text/html;profile=mcp-app',
@@ -475,7 +474,7 @@ export function ReviewResource() {
475
474
  </div>
476
475
 
477
476
  {/* Footer with Actions */}
478
- <div className="px-4 py-3 border-t border-[var(--color-border-tertiary)] bg-[var(--color-background-primary)]">
477
+ <div className="px-4 py-3 border-t border-[var(--color-border-tertiary)]">
479
478
  {decision === null ? (
480
479
  <div className="flex gap-3">
481
480
  <Button
@@ -0,0 +1,17 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { AuthInfo } from 'sunpeak/mcp';
3
+
4
+ /**
5
+ * Optional server entry point.
6
+ *
7
+ * Called on every MCP request. Return AuthInfo to authenticate, null to reject (401).
8
+ * The returned AuthInfo is available as `extra.authInfo` in tool handlers.
9
+ */
10
+ export async function auth(req: IncomingMessage): Promise<AuthInfo | null> {
11
+ const token = req.headers.authorization?.replace('Bearer ', '');
12
+ // Allow unauthenticated requests (no token = anonymous access).
13
+ // To require auth, return null here instead.
14
+ return { token: token ?? '', clientId: 'anonymous', scopes: [] };
15
+ }
16
+
17
+ export const server = { name: 'Sunpeak', version: '1.0.0' };
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'review',
6
+ title: 'Diff Review',
7
+ description: 'Show a review dialog for a proposed code diff',
8
+ annotations: { readOnlyHint: false },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ changesetId: z.string().describe('Unique identifier for the changeset'),
16
+ title: z.string().describe('Title describing the changes'),
17
+ description: z.string().describe('Detailed description of what the changes accomplish'),
18
+ files: z.array(z.string()).describe('List of file paths affected by this change'),
19
+ runMigrations: z.boolean().describe('Whether to run database migrations as part of the change'),
20
+ };
21
+
22
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
23
+ return { structuredContent: { title: 'Review', sections: [] } };
24
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'review',
6
+ title: 'Review Post',
7
+ description: 'Review a social media post before publishing',
8
+ annotations: { readOnlyHint: false },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ content: z.string().describe('The text content of the post'),
16
+ platforms: z
17
+ .array(z.enum(['x', 'linkedin', 'facebook', 'instagram']))
18
+ .describe('Social media platforms to post to'),
19
+ schedule: z.enum(['now', 'scheduled']).describe('When to publish the post'),
20
+ scheduledTime: z.string().describe('ISO 8601 timestamp for scheduled posts'),
21
+ visibility: z.enum(['public', 'connections', 'private']).describe('Post visibility setting'),
22
+ };
23
+
24
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
25
+ return { structuredContent: { title: 'Review Your Post', sections: [] } };
26
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'review',
6
+ title: 'Review Purchase',
7
+ description: 'Review a purchase before completing the transaction',
8
+ annotations: { readOnlyHint: false },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ cartId: z.string().describe('Shopping cart identifier'),
16
+ items: z
17
+ .array(
18
+ z.object({
19
+ productId: z.string(),
20
+ quantity: z.number(),
21
+ })
22
+ )
23
+ .describe('List of items to purchase'),
24
+ shippingAddressId: z.string().describe('ID of the saved shipping address'),
25
+ shippingMethod: z.enum(['standard', 'express', 'overnight']).describe('Shipping speed'),
26
+ paymentMethodId: z.string().describe('ID of the saved payment method'),
27
+ };
28
+
29
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
30
+ return { structuredContent: { title: 'Confirm Your Order', sections: [] } };
31
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'albums',
6
+ title: 'Show Albums',
7
+ description: 'Show photo albums',
8
+ annotations: { readOnlyHint: true },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ category: z.string().describe('Filter albums by category (e.g., travel, food, family)'),
16
+ search: z.string().describe('Search term to filter albums by title or description'),
17
+ limit: z.number().describe('Maximum number of albums to return'),
18
+ };
19
+
20
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
21
+ return { structuredContent: { albums: [] } };
22
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'carousel',
6
+ title: 'Show Carousel',
7
+ description: 'Show popular places to visit',
8
+ annotations: { readOnlyHint: true },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ city: z.string().describe('City name to search for places'),
16
+ state: z.string().describe('State or region'),
17
+ categories: z
18
+ .array(z.string())
19
+ .describe('Filter by categories (e.g., parks, restaurants, landmarks)'),
20
+ limit: z.number().describe('Maximum number of places to return'),
21
+ };
22
+
23
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
24
+ return { structuredContent: { places: [] } };
25
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
3
+
4
+ export const tool: AppToolConfig = {
5
+ resource: 'map',
6
+ title: 'Show Map',
7
+ description: 'Show the map',
8
+ annotations: { readOnlyHint: true },
9
+ _meta: {
10
+ ui: { visibility: ['model', 'app'] },
11
+ },
12
+ };
13
+
14
+ export const schema = {
15
+ query: z.string().describe('Search query for places (e.g., pizza, coffee, restaurants)'),
16
+ location: z
17
+ .object({
18
+ lat: z.number(),
19
+ lng: z.number(),
20
+ })
21
+ .describe('Center location for the search'),
22
+ radius: z.number().describe('Search radius in miles'),
23
+ minRating: z.number().describe('Minimum rating filter (1-5)'),
24
+ priceRange: z.array(z.enum(['$', '$$', '$$$', '$$$$'])).describe('Price range filter'),
25
+ };
26
+
27
+ export default async function (_args: Record<string, unknown>, _extra: ToolHandlerExtra) {
28
+ return { structuredContent: { places: [] } };
29
+ }
@@ -7,7 +7,7 @@ for (const host of hosts) {
7
7
  test.describe(`Albums Resource [${host}]`, () => {
8
8
  test.describe('Light Mode', () => {
9
9
  test('should render album cards with correct styles', async ({ page }) => {
10
- await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'light', host }));
10
+ await page.goto(createSimulatorUrl({ simulation: 'show-albums', theme: 'light', host }));
11
11
 
12
12
  const iframe = page.frameLocator('iframe');
13
13
  const albumCard = iframe.locator('button:has-text("Summer Slice")');
@@ -27,7 +27,7 @@ for (const host of hosts) {
27
27
  });
28
28
 
29
29
  test('should have album image with correct aspect ratio', async ({ page }) => {
30
- await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'light', host }));
30
+ await page.goto(createSimulatorUrl({ simulation: 'show-albums', theme: 'light', host }));
31
31
 
32
32
  const iframe = page.frameLocator('iframe');
33
33
  const albumImage = iframe.locator('button:has-text("Summer Slice") img').first();
@@ -54,7 +54,7 @@ for (const host of hosts) {
54
54
 
55
55
  test.describe('Dark Mode', () => {
56
56
  test('should render album cards with correct styles', async ({ page }) => {
57
- await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'dark', host }));
57
+ await page.goto(createSimulatorUrl({ simulation: 'show-albums', theme: 'dark', host }));
58
58
 
59
59
  const iframe = page.frameLocator('iframe');
60
60
  const albumCard = iframe.locator('button:has-text("Summer Slice")');
@@ -73,7 +73,7 @@ for (const host of hosts) {
73
73
  });
74
74
 
75
75
  test('should have text with appropriate contrast', async ({ page }) => {
76
- await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'dark', host }));
76
+ await page.goto(createSimulatorUrl({ simulation: 'show-albums', theme: 'dark', host }));
77
77
 
78
78
  const iframe = page.frameLocator('iframe');
79
79
  const albumTitle = iframe.locator('button:has-text("Summer Slice") div').first();
@@ -96,7 +96,7 @@ for (const host of hosts) {
96
96
  test('should render correctly in fullscreen displayMode', async ({ page }) => {
97
97
  await page.goto(
98
98
  createSimulatorUrl({
99
- simulation: 'albums-show',
99
+ simulation: 'show-albums',
100
100
  theme: 'light',
101
101
  displayMode: 'fullscreen',
102
102
  host,
@@ -114,7 +114,7 @@ for (const host of hosts) {
114
114
  test('should maintain album card styles in fullscreen', async ({ page }) => {
115
115
  await page.goto(
116
116
  createSimulatorUrl({
117
- simulation: 'albums-show',
117
+ simulation: 'show-albums',
118
118
  theme: 'dark',
119
119
  displayMode: 'fullscreen',
120
120
  host,
@@ -7,7 +7,7 @@ for (const host of hosts) {
7
7
  test.describe(`Carousel Resource [${host}]`, () => {
8
8
  test.describe('Light Mode', () => {
9
9
  test('should render carousel cards with correct styles', async ({ page }) => {
10
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light', host }));
10
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'light', host }));
11
11
 
12
12
  const iframe = page.frameLocator('iframe');
13
13
  const card = iframe.locator('.rounded-2xl').first();
@@ -26,7 +26,7 @@ for (const host of hosts) {
26
26
  });
27
27
 
28
28
  test('should have card with border styling', async ({ page }) => {
29
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light', host }));
29
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'light', host }));
30
30
 
31
31
  const iframe = page.frameLocator('iframe');
32
32
  const card = iframe.locator('.rounded-2xl.border').first();
@@ -45,7 +45,7 @@ for (const host of hosts) {
45
45
  });
46
46
 
47
47
  test('should have interactive buttons', async ({ page }) => {
48
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light', host }));
48
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'light', host }));
49
49
 
50
50
  const iframe = page.frameLocator('iframe');
51
51
  const visitButton = iframe.locator('button:has-text("Visit")').first();
@@ -64,7 +64,7 @@ for (const host of hosts) {
64
64
 
65
65
  test.describe('Dark Mode', () => {
66
66
  test('should render carousel cards with correct styles', async ({ page }) => {
67
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark', host }));
67
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'dark', host }));
68
68
 
69
69
  const iframe = page.frameLocator('iframe');
70
70
  const card = iframe.locator('.rounded-2xl').first();
@@ -83,7 +83,7 @@ for (const host of hosts) {
83
83
  });
84
84
 
85
85
  test('should have appropriate styling for dark mode', async ({ page }) => {
86
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark', host }));
86
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'dark', host }));
87
87
 
88
88
  const iframe = page.frameLocator('iframe');
89
89
  // Select card by its border + rounded combo
@@ -110,7 +110,7 @@ for (const host of hosts) {
110
110
  }
111
111
  });
112
112
 
113
- await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark', host }));
113
+ await page.goto(createSimulatorUrl({ simulation: 'show-carousel', theme: 'dark', host }));
114
114
 
115
115
  // Wait for iframe content to render
116
116
  const iframe = page.frameLocator('iframe');