pulumi-package-docs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/bin/cli.js +80 -0
  4. package/dist/client/assets/MemberDetail-CX9jiyic.js +1 -0
  5. package/dist/client/assets/index-CZRUHwIq.js +13 -0
  6. package/dist/client/assets/providers._name-DpU324kz.js +1 -0
  7. package/dist/client/assets/providers._name.functions._token-DXrP4mSE.js +1 -0
  8. package/dist/client/assets/providers._name.functions._token-bvc3vrG2.js +1 -0
  9. package/dist/client/assets/providers._name.index-CtKE1x7S.js +1 -0
  10. package/dist/client/assets/providers._name.index-tTXQzRr7.js +1 -0
  11. package/dist/client/assets/providers._name.resources._token--BZM8RBv.js +1 -0
  12. package/dist/client/assets/providers._name.resources._token-CbZkRkaE.js +1 -0
  13. package/dist/client/assets/routes-GAgiQcBX.js +1 -0
  14. package/dist/client/assets/styles-CWDprFhI.css +1 -0
  15. package/dist/client/assets/token-oQmZfAP3.js +1 -0
  16. package/dist/client/assets/useSelector-BIt42tGI.js +1 -0
  17. package/dist/server/assets/MemberDetail-BiBAI1vw.js +96 -0
  18. package/dist/server/assets/_tanstack-start-manifest_v-Cl1i6HDY.js +62 -0
  19. package/dist/server/assets/api-BV6lykkP.js +20 -0
  20. package/dist/server/assets/api-C55bS4R6.js +64 -0
  21. package/dist/server/assets/empty-plugin-adapters-D9UWiqvJ.js +5 -0
  22. package/dist/server/assets/providers._name-DF6sA3DG.js +6 -0
  23. package/dist/server/assets/providers._name.functions._token-C0A7Y90t.js +21 -0
  24. package/dist/server/assets/providers._name.functions._token-D8LsoIum.js +13 -0
  25. package/dist/server/assets/providers._name.index-BEwt-7U0.js +13 -0
  26. package/dist/server/assets/providers._name.index-BK_ZRd5l.js +97 -0
  27. package/dist/server/assets/providers._name.resources._token-CbvRkms4.js +22 -0
  28. package/dist/server/assets/providers._name.resources._token-DJPF8nMg.js +13 -0
  29. package/dist/server/assets/router-DZSocx9m.js +562 -0
  30. package/dist/server/assets/routes-B-G_iLTo.js +82 -0
  31. package/dist/server/assets/schema.server-B-VvU1qb.js +122 -0
  32. package/dist/server/assets/start-5Z2QO8AU.js +4 -0
  33. package/dist/server/server.js +2003 -0
  34. package/package.json +67 -0
  35. package/src/components/CodeExample.tsx +14 -0
  36. package/src/components/MemberDetail.tsx +89 -0
  37. package/src/components/PropertyTable.tsx +49 -0
  38. package/src/components/PulumiLogo.tsx +86 -0
  39. package/src/lib/pulumi/api.ts +77 -0
  40. package/src/lib/pulumi/description.test.ts +54 -0
  41. package/src/lib/pulumi/description.ts +35 -0
  42. package/src/lib/pulumi/discovery.server.test.ts +117 -0
  43. package/src/lib/pulumi/discovery.server.ts +77 -0
  44. package/src/lib/pulumi/examples.test.ts +97 -0
  45. package/src/lib/pulumi/examples.ts +55 -0
  46. package/src/lib/pulumi/format.test.ts +51 -0
  47. package/src/lib/pulumi/format.ts +11 -0
  48. package/src/lib/pulumi/mcp.server.ts +343 -0
  49. package/src/lib/pulumi/naming.test.ts +27 -0
  50. package/src/lib/pulumi/naming.ts +31 -0
  51. package/src/lib/pulumi/schema.server.ts +78 -0
  52. package/src/lib/pulumi/token.test.ts +21 -0
  53. package/src/lib/pulumi/token.ts +10 -0
  54. package/src/lib/pulumi/types.ts +62 -0
  55. package/src/routeTree.gen.ts +187 -0
  56. package/src/router.tsx +19 -0
  57. package/src/routes/__root.tsx +59 -0
  58. package/src/routes/index.tsx +78 -0
  59. package/src/routes/mcp.ts +30 -0
  60. package/src/routes/providers.$name.functions.$token.tsx +47 -0
  61. package/src/routes/providers.$name.index.tsx +129 -0
  62. package/src/routes/providers.$name.resources.$token.tsx +48 -0
  63. package/src/routes/providers.$name.tsx +5 -0
  64. package/src/styles.css +478 -0
  65. package/tsconfig.json +29 -0
@@ -0,0 +1,78 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { promisify } from 'node:util'
7
+
8
+ import type {
9
+ LocalPackageRef,
10
+ ProviderEntry,
11
+ PulumiSchema,
12
+ } from '@/lib/pulumi/types'
13
+
14
+ const execFileAsync = promisify(execFile)
15
+
16
+ const CLI_TIMEOUT_MS = 60_000
17
+ const MAX_BUFFER_BYTES = 64 * 1024 * 1024
18
+
19
+ function cacheFilePath(projectRoot: string, ref: LocalPackageRef): string {
20
+ const projectHash = createHash('sha256')
21
+ .update(projectRoot)
22
+ .digest('hex')
23
+ .slice(0, 16)
24
+ const dir = join(tmpdir(), 'pulumi-package-docs', projectHash)
25
+ mkdirSync(dir, { recursive: true })
26
+ return join(dir, `${ref.name}.json`)
27
+ }
28
+
29
+ function readCache(path: string): PulumiSchema | null {
30
+ if (!existsSync(path)) return null
31
+ try {
32
+ return JSON.parse(readFileSync(path, 'utf-8')) as PulumiSchema
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ async function fetchViaCli(
39
+ projectRoot: string,
40
+ ref: LocalPackageRef,
41
+ ): Promise<PulumiSchema> {
42
+ const args = ['package', 'get-schema', ref.source, ...(ref.parameters ?? [])]
43
+ const { stdout } = await execFileAsync('pulumi', args, {
44
+ cwd: projectRoot,
45
+ timeout: CLI_TIMEOUT_MS,
46
+ maxBuffer: MAX_BUFFER_BYTES,
47
+ })
48
+ return JSON.parse(stdout) as PulumiSchema
49
+ }
50
+
51
+ /**
52
+ * Resolves the schema for a local package: try the `pulumi` CLI first (it's the
53
+ * source of truth and works for parameterized providers), and fall back to the
54
+ * last schema we successfully cached for this project if the CLI call fails.
55
+ */
56
+ export async function resolveProviderSchema(
57
+ projectRoot: string,
58
+ ref: LocalPackageRef,
59
+ ): Promise<ProviderEntry> {
60
+ const cachePath = cacheFilePath(projectRoot, ref)
61
+
62
+ try {
63
+ const schema = await fetchViaCli(projectRoot, ref)
64
+ writeFileSync(cachePath, JSON.stringify(schema))
65
+ return { ref, schema, origin: 'cli' }
66
+ } catch (cliError) {
67
+ const cached = readCache(cachePath)
68
+ if (cached) {
69
+ return { ref, schema: cached, origin: 'cache' }
70
+ }
71
+ return {
72
+ ref,
73
+ schema: null,
74
+ origin: 'error',
75
+ error: cliError instanceof Error ? cliError.message : String(cliError),
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { tokenDisplayName } from '@/lib/pulumi/token'
4
+
5
+ describe('tokenDisplayName', () => {
6
+ it('returns the segment after the last colon for a resource token', () => {
7
+ expect(tokenDisplayName('random:index/randomString:RandomString')).toBe(
8
+ 'RandomString',
9
+ )
10
+ })
11
+
12
+ it('returns the segment after the last colon for a function token', () => {
13
+ expect(
14
+ tokenDisplayName('random:index/randomInteger:getRandomInteger'),
15
+ ).toBe('getRandomInteger')
16
+ })
17
+
18
+ it('returns the token unchanged when it has no colon', () => {
19
+ expect(tokenDisplayName('plainName')).toBe('plainName')
20
+ })
21
+ })
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Schema tokens look like `random:index/randomString:RandomString` (resources)
3
+ * or `random:index/randomInteger:randomInteger` (functions). The segment after
4
+ * the last colon is the name every generated SDK exports for that member, and
5
+ * is unique within the provider, so it makes a readable display name.
6
+ */
7
+ export function tokenDisplayName(token: string): string {
8
+ const short = token.split(':').pop()
9
+ return short && short.length > 0 ? short : token
10
+ }
@@ -0,0 +1,62 @@
1
+ /** A locally-added package entry from a project's Pulumi.yaml `packages` section. */
2
+ export interface LocalPackageRef {
3
+ /** The local alias the project uses for this package, e.g. "random". */
4
+ name: string
5
+ /** The base provider/plugin this package resolves through, e.g. "terraform-provider". */
6
+ source: string
7
+ version?: string
8
+ /** Parameterization arguments, e.g. ["hashicorp/random"] for a bridged Terraform provider. */
9
+ parameters?: string[]
10
+ }
11
+
12
+ export interface PulumiProject {
13
+ root: string
14
+ name?: string
15
+ runtime?: string
16
+ packages: LocalPackageRef[]
17
+ }
18
+
19
+ export interface SchemaProperty {
20
+ type?: string
21
+ $ref?: string
22
+ description?: string
23
+ items?: SchemaProperty
24
+ }
25
+
26
+ export interface SchemaResource {
27
+ description?: string
28
+ properties?: Record<string, SchemaProperty>
29
+ required?: string[]
30
+ inputProperties?: Record<string, SchemaProperty>
31
+ requiredInputs?: string[]
32
+ deprecationMessage?: string
33
+ }
34
+
35
+ export interface SchemaFunction {
36
+ description?: string
37
+ inputs?: {
38
+ properties?: Record<string, SchemaProperty>
39
+ required?: string[]
40
+ }
41
+ outputs?: {
42
+ properties?: Record<string, SchemaProperty>
43
+ required?: string[]
44
+ }
45
+ }
46
+
47
+ export interface PulumiSchema {
48
+ name: string
49
+ version?: string
50
+ description?: string
51
+ resources?: Record<string, SchemaResource>
52
+ functions?: Record<string, SchemaFunction>
53
+ }
54
+
55
+ export type SchemaOrigin = 'cli' | 'cache'
56
+
57
+ export interface ProviderEntry {
58
+ ref: LocalPackageRef
59
+ schema: PulumiSchema | null
60
+ origin: SchemaOrigin | 'error'
61
+ error?: string
62
+ }
@@ -0,0 +1,187 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+ import { Route as McpRouteImport } from './routes/mcp'
14
+ import { Route as ProvidersNameRouteImport } from './routes/providers.$name'
15
+ import { Route as ProvidersNameIndexRouteImport } from './routes/providers.$name.index'
16
+ import { Route as ProvidersNameFunctionsTokenRouteImport } from './routes/providers.$name.functions.$token'
17
+ import { Route as ProvidersNameResourcesTokenRouteImport } from './routes/providers.$name.resources.$token'
18
+
19
+ const IndexRoute = IndexRouteImport.update({
20
+ id: '/',
21
+ path: '/',
22
+ getParentRoute: () => rootRouteImport,
23
+ } as any)
24
+ const McpRoute = McpRouteImport.update({
25
+ id: '/mcp',
26
+ path: '/mcp',
27
+ getParentRoute: () => rootRouteImport,
28
+ } as any)
29
+ const ProvidersNameRoute = ProvidersNameRouteImport.update({
30
+ id: '/providers/$name',
31
+ path: '/providers/$name',
32
+ getParentRoute: () => rootRouteImport,
33
+ } as any)
34
+ const ProvidersNameIndexRoute = ProvidersNameIndexRouteImport.update({
35
+ id: '/',
36
+ path: '/',
37
+ getParentRoute: () => ProvidersNameRoute,
38
+ } as any)
39
+ const ProvidersNameFunctionsTokenRoute =
40
+ ProvidersNameFunctionsTokenRouteImport.update({
41
+ id: '/functions/$token',
42
+ path: '/functions/$token',
43
+ getParentRoute: () => ProvidersNameRoute,
44
+ } as any)
45
+ const ProvidersNameResourcesTokenRoute =
46
+ ProvidersNameResourcesTokenRouteImport.update({
47
+ id: '/resources/$token',
48
+ path: '/resources/$token',
49
+ getParentRoute: () => ProvidersNameRoute,
50
+ } as any)
51
+
52
+ export interface FileRoutesByFullPath {
53
+ '/': typeof IndexRoute
54
+ '/mcp': typeof McpRoute
55
+ '/providers/$name': typeof ProvidersNameRouteWithChildren
56
+ '/providers/$name/': typeof ProvidersNameIndexRoute
57
+ '/providers/$name/functions/$token': typeof ProvidersNameFunctionsTokenRoute
58
+ '/providers/$name/resources/$token': typeof ProvidersNameResourcesTokenRoute
59
+ }
60
+ export interface FileRoutesByTo {
61
+ '/': typeof IndexRoute
62
+ '/mcp': typeof McpRoute
63
+ '/providers/$name': typeof ProvidersNameIndexRoute
64
+ '/providers/$name/functions/$token': typeof ProvidersNameFunctionsTokenRoute
65
+ '/providers/$name/resources/$token': typeof ProvidersNameResourcesTokenRoute
66
+ }
67
+ export interface FileRoutesById {
68
+ __root__: typeof rootRouteImport
69
+ '/': typeof IndexRoute
70
+ '/mcp': typeof McpRoute
71
+ '/providers/$name': typeof ProvidersNameRouteWithChildren
72
+ '/providers/$name/': typeof ProvidersNameIndexRoute
73
+ '/providers/$name/functions/$token': typeof ProvidersNameFunctionsTokenRoute
74
+ '/providers/$name/resources/$token': typeof ProvidersNameResourcesTokenRoute
75
+ }
76
+ export interface FileRouteTypes {
77
+ fileRoutesByFullPath: FileRoutesByFullPath
78
+ fullPaths:
79
+ | '/'
80
+ | '/mcp'
81
+ | '/providers/$name'
82
+ | '/providers/$name/'
83
+ | '/providers/$name/functions/$token'
84
+ | '/providers/$name/resources/$token'
85
+ fileRoutesByTo: FileRoutesByTo
86
+ to:
87
+ | '/'
88
+ | '/mcp'
89
+ | '/providers/$name'
90
+ | '/providers/$name/functions/$token'
91
+ | '/providers/$name/resources/$token'
92
+ id:
93
+ | '__root__'
94
+ | '/'
95
+ | '/mcp'
96
+ | '/providers/$name'
97
+ | '/providers/$name/'
98
+ | '/providers/$name/functions/$token'
99
+ | '/providers/$name/resources/$token'
100
+ fileRoutesById: FileRoutesById
101
+ }
102
+ export interface RootRouteChildren {
103
+ IndexRoute: typeof IndexRoute
104
+ McpRoute: typeof McpRoute
105
+ ProvidersNameRoute: typeof ProvidersNameRouteWithChildren
106
+ }
107
+
108
+ declare module '@tanstack/react-router' {
109
+ interface FileRoutesByPath {
110
+ '/': {
111
+ id: '/'
112
+ path: '/'
113
+ fullPath: '/'
114
+ preLoaderRoute: typeof IndexRouteImport
115
+ parentRoute: typeof rootRouteImport
116
+ }
117
+ '/mcp': {
118
+ id: '/mcp'
119
+ path: '/mcp'
120
+ fullPath: '/mcp'
121
+ preLoaderRoute: typeof McpRouteImport
122
+ parentRoute: typeof rootRouteImport
123
+ }
124
+ '/providers/$name': {
125
+ id: '/providers/$name'
126
+ path: '/providers/$name'
127
+ fullPath: '/providers/$name'
128
+ preLoaderRoute: typeof ProvidersNameRouteImport
129
+ parentRoute: typeof rootRouteImport
130
+ }
131
+ '/providers/$name/': {
132
+ id: '/providers/$name/'
133
+ path: '/'
134
+ fullPath: '/providers/$name/'
135
+ preLoaderRoute: typeof ProvidersNameIndexRouteImport
136
+ parentRoute: typeof ProvidersNameRoute
137
+ }
138
+ '/providers/$name/functions/$token': {
139
+ id: '/providers/$name/functions/$token'
140
+ path: '/functions/$token'
141
+ fullPath: '/providers/$name/functions/$token'
142
+ preLoaderRoute: typeof ProvidersNameFunctionsTokenRouteImport
143
+ parentRoute: typeof ProvidersNameRoute
144
+ }
145
+ '/providers/$name/resources/$token': {
146
+ id: '/providers/$name/resources/$token'
147
+ path: '/resources/$token'
148
+ fullPath: '/providers/$name/resources/$token'
149
+ preLoaderRoute: typeof ProvidersNameResourcesTokenRouteImport
150
+ parentRoute: typeof ProvidersNameRoute
151
+ }
152
+ }
153
+ }
154
+
155
+ interface ProvidersNameRouteChildren {
156
+ ProvidersNameIndexRoute: typeof ProvidersNameIndexRoute
157
+ ProvidersNameFunctionsTokenRoute: typeof ProvidersNameFunctionsTokenRoute
158
+ ProvidersNameResourcesTokenRoute: typeof ProvidersNameResourcesTokenRoute
159
+ }
160
+
161
+ const ProvidersNameRouteChildren: ProvidersNameRouteChildren = {
162
+ ProvidersNameIndexRoute: ProvidersNameIndexRoute,
163
+ ProvidersNameFunctionsTokenRoute: ProvidersNameFunctionsTokenRoute,
164
+ ProvidersNameResourcesTokenRoute: ProvidersNameResourcesTokenRoute,
165
+ }
166
+
167
+ const ProvidersNameRouteWithChildren = ProvidersNameRoute._addFileChildren(
168
+ ProvidersNameRouteChildren,
169
+ )
170
+
171
+ const rootRouteChildren: RootRouteChildren = {
172
+ IndexRoute: IndexRoute,
173
+ McpRoute: McpRoute,
174
+ ProvidersNameRoute: ProvidersNameRouteWithChildren,
175
+ }
176
+ export const routeTree = rootRouteImport
177
+ ._addFileChildren(rootRouteChildren)
178
+ ._addFileTypes<FileRouteTypes>()
179
+
180
+ import type { getRouter } from './router.tsx'
181
+ import type { createStart } from '@tanstack/react-start'
182
+ declare module '@tanstack/react-start' {
183
+ interface Register {
184
+ ssr: true
185
+ router: Awaited<ReturnType<typeof getRouter>>
186
+ }
187
+ }
package/src/router.tsx ADDED
@@ -0,0 +1,19 @@
1
+ import { createRouter as createTanStackRouter } from '@tanstack/react-router'
2
+ import { routeTree } from './routeTree.gen'
3
+
4
+ export function getRouter() {
5
+ const router = createTanStackRouter({
6
+ routeTree,
7
+ scrollRestoration: true,
8
+ defaultPreload: 'intent',
9
+ defaultPreloadStaleTime: 0,
10
+ })
11
+
12
+ return router
13
+ }
14
+
15
+ declare module '@tanstack/react-router' {
16
+ interface Register {
17
+ router: ReturnType<typeof getRouter>
18
+ }
19
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ HeadContent,
3
+ Link,
4
+ Scripts,
5
+ createRootRoute,
6
+ } from '@tanstack/react-router'
7
+
8
+ import { PulumiLogo } from '@/components/PulumiLogo'
9
+ import appCss from '../styles.css?url'
10
+
11
+ export const Route = createRootRoute({
12
+ head: () => ({
13
+ meta: [
14
+ {
15
+ charSet: 'utf-8',
16
+ },
17
+ {
18
+ name: 'viewport',
19
+ content: 'width=device-width, initial-scale=1',
20
+ },
21
+ {
22
+ title: 'Local Provider Docs',
23
+ },
24
+ ],
25
+ links: [
26
+ {
27
+ rel: 'stylesheet',
28
+ href: appCss,
29
+ },
30
+ {
31
+ rel: 'icon',
32
+ href: 'https://brand.pulumi.com/media/images/logos/icon-rounded-64w.png',
33
+ },
34
+ ],
35
+ }),
36
+ shellComponent: RootDocument,
37
+ })
38
+
39
+ function RootDocument({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <html lang="en">
42
+ <head>
43
+ <HeadContent />
44
+ </head>
45
+ <body>
46
+ <header className="site-header">
47
+ <Link to="/" className="site-brand">
48
+ <PulumiLogo height={22} />
49
+ <span className="site-brand-divider" aria-hidden="true" />
50
+ <span className="site-brand-name">Local Provider Docs</span>
51
+ </Link>
52
+ </header>
53
+ {children}
54
+
55
+ <Scripts />
56
+ </body>
57
+ </html>
58
+ )
59
+ }
@@ -0,0 +1,78 @@
1
+ import { Link, createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { listProviders } from '@/lib/pulumi/api'
4
+
5
+ export const Route = createFileRoute('/')({
6
+ loader: () => listProviders(),
7
+ component: Home,
8
+ })
9
+
10
+ function Home() {
11
+ const { targetDir, project, providers } = Route.useLoaderData()
12
+
13
+ if (!project) {
14
+ return (
15
+ <main>
16
+ <h1>Local Provider Docs</h1>
17
+ <p className="empty-state">
18
+ No Pulumi project was found at <code>{targetDir}</code> (or any parent
19
+ directory). Run this from inside a Pulumi project directory, or with{' '}
20
+ <code>--dir &lt;path&gt;</code> pointing at one.
21
+ </p>
22
+ </main>
23
+ )
24
+ }
25
+
26
+ return (
27
+ <main>
28
+ <h1>Local Provider Docs</h1>
29
+ <p className="subtitle">
30
+ {project.name ?? 'Pulumi project'} &middot; <code>{project.root}</code>
31
+ </p>
32
+
33
+ {providers.length === 0 ? (
34
+ <p className="empty-state">
35
+ This project's <code>Pulumi.yaml</code> has no <code>packages</code>{' '}
36
+ section, so there are no local or parameterized providers to show docs
37
+ for yet. Add one with <code>pulumi package add</code>.
38
+ </p>
39
+ ) : (
40
+ <ul className="provider-list">
41
+ {providers.map((provider) => (
42
+ <li key={provider.name} className="provider-card">
43
+ <Link to="/providers/$name" params={{ name: provider.name }}>
44
+ <span className="provider-name">{provider.name}</span>
45
+ <span className="provider-source">
46
+ {provider.source}
47
+ {provider.version ? ` @ ${provider.version}` : ''}
48
+ {provider.parameters?.length
49
+ ? ` (${provider.parameters.join(' ')})`
50
+ : ''}
51
+ </span>
52
+ </Link>
53
+ {provider.origin === 'error' ? (
54
+ <p className="banner banner-failure">
55
+ Failed to load schema: {provider.error}
56
+ </p>
57
+ ) : (
58
+ <p className="provider-stats">
59
+ {provider.resourceCount} resource
60
+ {provider.resourceCount === 1 ? '' : 's'}
61
+ {' · '}
62
+ {provider.functionCount} function
63
+ {provider.functionCount === 1 ? '' : 's'}
64
+ {provider.origin === 'cache' && (
65
+ <>
66
+ {' · '}
67
+ <span className="badge badge-info">from cache</span>
68
+ </>
69
+ )}
70
+ </p>
71
+ )}
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ )}
76
+ </main>
77
+ )
78
+ }
@@ -0,0 +1,30 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
4
+
5
+ import { createMcpServer } from '@/lib/pulumi/mcp.server'
6
+
7
+ /**
8
+ * Stateless MCP endpoint: a fresh server + transport per request, so concurrent
9
+ * requests never share state. There's nothing here worth persisting across
10
+ * requests (schemas are re-resolved, cached on disk, each call) and every tool
11
+ * is a plain read, so statelessness keeps this simple.
12
+ */
13
+ async function handleMcpRequest({ request }: { request: Request }) {
14
+ const server = createMcpServer()
15
+ const transport = new WebStandardStreamableHTTPServerTransport({
16
+ sessionIdGenerator: undefined,
17
+ })
18
+ await server.connect(transport)
19
+ return transport.handleRequest(request)
20
+ }
21
+
22
+ export const Route = createFileRoute('/mcp')({
23
+ server: {
24
+ handlers: {
25
+ GET: handleMcpRequest,
26
+ POST: handleMcpRequest,
27
+ DELETE: handleMcpRequest,
28
+ },
29
+ },
30
+ })
@@ -0,0 +1,47 @@
1
+ import { Link, createFileRoute, notFound } from '@tanstack/react-router'
2
+
3
+ import { MemberDetail } from '@/components/MemberDetail'
4
+ import { getProvider } from '@/lib/pulumi/api'
5
+ import { tokenDisplayName } from '@/lib/pulumi/token'
6
+
7
+ export const Route = createFileRoute('/providers/$name/functions/$token')({
8
+ loader: async ({ params }) => {
9
+ const entry = await getProvider({ data: params.name })
10
+ const fn = entry?.schema?.functions?.[params.token]
11
+ if (!fn) throw notFound()
12
+ return {
13
+ providerName: params.name,
14
+ token: params.token,
15
+ fn,
16
+ runtime: entry?.runtime,
17
+ }
18
+ },
19
+ component: FunctionDetail,
20
+ notFoundComponent: () => (
21
+ <main>
22
+ <p>
23
+ No function with that name was found.{' '}
24
+ <Link to="/">Back to all providers.</Link>
25
+ </p>
26
+ </main>
27
+ ),
28
+ })
29
+
30
+ function FunctionDetail() {
31
+ const { providerName, token, fn, runtime } = Route.useLoaderData()
32
+
33
+ return (
34
+ <MemberDetail
35
+ providerName={providerName}
36
+ kind="Function"
37
+ displayName={tokenDisplayName(token)}
38
+ token={token}
39
+ description={fn.description}
40
+ runtime={runtime}
41
+ inputProperties={fn.inputs?.properties}
42
+ requiredInputs={fn.inputs?.required}
43
+ outputProperties={fn.outputs?.properties}
44
+ requiredOutputs={fn.outputs?.required}
45
+ />
46
+ )
47
+ }