@igstack/app-catalog-frontend-core 1.0.0-alpha-20260913172344 → 1.0.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 (37) hide show
  1. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +1 -1
  2. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
  3. package/dist/esm/modules/appCatalog/ui/components/Highlight.js +1 -1
  4. package/dist/esm/modules/appCatalog/ui/components/Highlight.js.map +1 -1
  5. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +1 -1
  6. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +11 -0
  7. package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/ui/pages/McpPage.d.ts +11 -0
  9. package/dist/esm/modules/appCatalog/ui/pages/McpPage.js +227 -0
  10. package/dist/esm/modules/appCatalog/ui/pages/McpPage.js.map +1 -0
  11. package/dist/esm/modules/appCatalog/utils/fetchMcpTools.d.ts +32 -0
  12. package/dist/esm/modules/appCatalog/utils/fetchMcpTools.js +79 -0
  13. package/dist/esm/modules/appCatalog/utils/fetchMcpTools.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/utils/highlightText.d.ts +15 -0
  15. package/dist/esm/modules/appCatalog/utils/highlightText.js +30 -0
  16. package/dist/esm/modules/appCatalog/utils/highlightText.js.map +1 -0
  17. package/dist/esm/routeTree.gen.d.ts +18 -3
  18. package/dist/esm/routeTree.gen.js +9 -2
  19. package/dist/esm/routeTree.gen.js.map +1 -1
  20. package/dist/esm/routes/_layout/mcp.d.ts +3 -0
  21. package/dist/esm/routes/_layout/mcp.js +20 -0
  22. package/dist/esm/routes/_layout/mcp.js.map +1 -0
  23. package/package.json +5 -5
  24. package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +1 -1
  25. package/src/modules/appCatalog/ui/components/Highlight.tsx +1 -1
  26. package/src/modules/appCatalog/ui/components/ViewToggle.tsx +10 -1
  27. package/src/modules/appCatalog/ui/pages/McpPage.tsx +275 -0
  28. package/src/modules/appCatalog/utils/fetchMcpTools.ts +140 -0
  29. package/src/modules/appCatalog/utils/highlightText.ts +51 -0
  30. package/src/routeTree.gen.ts +21 -0
  31. package/src/routes/_layout/mcp.tsx +22 -0
  32. package/dist/esm/__tests__/modules/appCatalog/utils/searchApps.test.d.ts +0 -1
  33. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +0 -49
  34. package/dist/esm/modules/appCatalog/utils/searchApps.js +0 -144
  35. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +0 -1
  36. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +0 -96
  37. package/src/modules/appCatalog/utils/searchApps.ts +0 -266
@@ -0,0 +1,275 @@
1
+ import { Check, Copy, TriangleAlert } from 'lucide-react'
2
+ import { useCallback, useEffect, useRef, useState } from 'react'
3
+ import { Button } from '~/ui/button'
4
+ import { Spinner } from '~/ui/spinner'
5
+ import type { McpTool } from '../../utils/fetchMcpTools'
6
+ import { MCP_ENDPOINT_PATH, fetchMcpTools } from '../../utils/fetchMcpTools'
7
+
8
+ /**
9
+ * MCP docs page: how to point an AI agent at this catalog.
10
+ *
11
+ * Every snippet is built from `window.location.origin`, so production, a
12
+ * preview host and localhost each show a config that actually works — nothing
13
+ * is hardcoded.
14
+ *
15
+ * The tool reference is fetched from the live registry (`tools/list`) rather
16
+ * than written out by hand, so it cannot rot when the server's tools change.
17
+ */
18
+ export function McpPage() {
19
+ const origin = typeof window === 'undefined' ? '' : window.location.origin
20
+ const endpoint = `${origin}${MCP_ENDPOINT_PATH}`
21
+ const serverName = 'app-catalog'
22
+
23
+ const [tools, setTools] = useState<McpTool[] | null>(null)
24
+ const [error, setError] = useState<string | null>(null)
25
+ const [reloadKey, setReloadKey] = useState(0)
26
+ const headingRef = useRef<HTMLHeadingElement>(null)
27
+
28
+ // New view: land keyboard and screen-reader users in the content rather than
29
+ // leaving focus on whatever nav element was clicked.
30
+ useEffect(() => {
31
+ headingRef.current?.focus()
32
+ }, [])
33
+
34
+ useEffect(() => {
35
+ const controller = new AbortController()
36
+ setTools(null)
37
+ setError(null)
38
+ fetchMcpTools(origin, controller.signal)
39
+ .then(setTools)
40
+ .catch((err: unknown) => {
41
+ if (controller.signal.aborted) return
42
+ setError(
43
+ err instanceof Error
44
+ ? err.message
45
+ : 'Could not reach the MCP endpoint.',
46
+ )
47
+ })
48
+ return () => controller.abort()
49
+ }, [origin, reloadKey])
50
+
51
+ const mcpJson = JSON.stringify(
52
+ {
53
+ mcpServers: {
54
+ [serverName]: { type: 'http', url: endpoint },
55
+ },
56
+ },
57
+ null,
58
+ 2,
59
+ )
60
+ const cliCommand = `claude mcp add --transport http ${serverName} ${endpoint}`
61
+
62
+ return (
63
+ <div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
64
+ <div className="flex flex-col gap-8 max-w-3xl pb-8">
65
+ <section className="flex flex-col gap-2">
66
+ <h1
67
+ ref={headingRef}
68
+ tabIndex={-1}
69
+ className="text-xl font-semibold outline-none"
70
+ >
71
+ MCP server
72
+ </h1>
73
+ <p className="text-sm text-muted-foreground">
74
+ This catalog exposes a{' '}
75
+ <a
76
+ href="https://modelcontextprotocol.io"
77
+ target="_blank"
78
+ rel="noopener noreferrer"
79
+ className="text-primary hover:underline"
80
+ >
81
+ Model Context Protocol
82
+ </a>{' '}
83
+ server, so an AI agent can search the catalog and read an entry the
84
+ same way you can — what exists, who owns it, and how to request
85
+ access. Point your agent at the endpoint below and it gains those
86
+ tools; no key or account is needed.
87
+ </p>
88
+ <p className="text-sm text-muted-foreground">
89
+ It speaks MCP over Streamable HTTP (JSON-RPC over{' '}
90
+ <code className="text-xs">POST</code>), and it is read-only.
91
+ </p>
92
+ </section>
93
+
94
+ <Snippet
95
+ title="Endpoint"
96
+ description="The URL an MCP client connects to."
97
+ value={endpoint}
98
+ />
99
+
100
+ <Snippet
101
+ title="Add it with the CLI"
102
+ description="One command, if your agent ships a CLI that manages MCP servers."
103
+ value={cliCommand}
104
+ />
105
+
106
+ <Snippet
107
+ title="Or add it to .mcp.json"
108
+ description="Commit this in a project so everyone working on it gets the same tools."
109
+ value={mcpJson}
110
+ language="json"
111
+ />
112
+
113
+ <section
114
+ className="flex flex-col gap-3"
115
+ aria-busy={tools === null && error === null}
116
+ >
117
+ <div className="flex flex-col gap-1">
118
+ <h2 className="text-base font-semibold">Tools</h2>
119
+ <p className="text-xs text-muted-foreground">
120
+ Read live from the server, so this list is always what your agent
121
+ will actually see.
122
+ </p>
123
+ </div>
124
+
125
+ {error !== null ? (
126
+ <div
127
+ role="alert"
128
+ className="flex flex-col items-start gap-3 rounded-md border border-destructive/40 bg-destructive/5 p-4"
129
+ >
130
+ <p className="flex items-start gap-2 text-sm">
131
+ <TriangleAlert className="size-4 shrink-0 mt-0.5 text-destructive" />
132
+ <span>
133
+ Could not read the tool list. {error} The setup instructions
134
+ above are still correct.
135
+ </span>
136
+ </p>
137
+ <Button
138
+ variant="outline"
139
+ size="sm"
140
+ onClick={() => setReloadKey((k) => k + 1)}
141
+ >
142
+ Try again
143
+ </Button>
144
+ </div>
145
+ ) : tools === null ? (
146
+ <p className="flex items-center gap-2 text-sm text-muted-foreground">
147
+ <Spinner className="size-4" />
148
+ Reading the tool list…
149
+ </p>
150
+ ) : tools.length === 0 ? (
151
+ <p className="text-sm text-muted-foreground">
152
+ The server reported no tools.
153
+ </p>
154
+ ) : (
155
+ <ul className="flex flex-col gap-4 list-none p-0 m-0">
156
+ {tools.map((tool) => (
157
+ <li
158
+ key={tool.name}
159
+ className="rounded-md border p-4 flex flex-col gap-2"
160
+ >
161
+ <code className="text-sm font-semibold">{tool.name}</code>
162
+ {tool.description && (
163
+ <p className="text-sm text-muted-foreground">
164
+ {tool.description}
165
+ </p>
166
+ )}
167
+ {tool.parameters.length === 0 ? (
168
+ <p className="text-xs text-muted-foreground">
169
+ No parameters.
170
+ </p>
171
+ ) : (
172
+ <dl className="flex flex-col gap-2 m-0">
173
+ {tool.parameters.map((param) => (
174
+ <div key={param.name} className="flex flex-col gap-0.5">
175
+ <dt className="text-xs">
176
+ <code className="font-medium">{param.name}</code>
177
+ {param.type && (
178
+ <span className="text-muted-foreground">
179
+ {' '}
180
+ {param.type}
181
+ </span>
182
+ )}
183
+ <span className="text-muted-foreground">
184
+ {' '}
185
+ — {param.required ? 'required' : 'optional'}
186
+ </span>
187
+ </dt>
188
+ {param.description && (
189
+ <dd className="text-xs text-muted-foreground m-0">
190
+ {param.description}
191
+ </dd>
192
+ )}
193
+ {param.enumValues && (
194
+ <dd className="text-xs text-muted-foreground m-0">
195
+ One of: {param.enumValues.join(', ')}
196
+ </dd>
197
+ )}
198
+ </div>
199
+ ))}
200
+ </dl>
201
+ )}
202
+ </li>
203
+ ))}
204
+ </ul>
205
+ )}
206
+ </section>
207
+ </div>
208
+ </div>
209
+ )
210
+ }
211
+
212
+ function Snippet({
213
+ title,
214
+ description,
215
+ value,
216
+ language,
217
+ }: {
218
+ title: string
219
+ description: string
220
+ value: string
221
+ language?: string
222
+ }) {
223
+ return (
224
+ <section className="flex flex-col gap-2">
225
+ <h2 className="text-base font-semibold">{title}</h2>
226
+ <p className="text-xs text-muted-foreground">{description}</p>
227
+ <div className="relative">
228
+ <pre
229
+ className="overflow-x-auto rounded-md border bg-muted/40 p-3 pr-12 text-xs"
230
+ data-language={language}
231
+ >
232
+ <code>{value}</code>
233
+ </pre>
234
+ <CopyButton value={value} label={title} />
235
+ </div>
236
+ </section>
237
+ )
238
+ }
239
+
240
+ function CopyButton({ value, label }: { value: string; label: string }) {
241
+ const [copied, setCopied] = useState(false)
242
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
243
+
244
+ useEffect(
245
+ () => () => {
246
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
247
+ },
248
+ [],
249
+ )
250
+
251
+ const copy = useCallback(() => {
252
+ void navigator.clipboard.writeText(value)
253
+ setCopied(true)
254
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
255
+ timeoutRef.current = setTimeout(() => setCopied(false), 2000)
256
+ }, [value])
257
+
258
+ return (
259
+ <Button
260
+ type="button"
261
+ variant="ghost"
262
+ size="icon"
263
+ onClick={copy}
264
+ aria-label={copied ? `${label} copied` : `Copy ${label}`}
265
+ title={copied ? 'Copied' : 'Copy'}
266
+ className="absolute right-1.5 top-1.5"
267
+ >
268
+ {copied ? (
269
+ <Check className="size-4 text-primary" />
270
+ ) : (
271
+ <Copy className="size-4" />
272
+ )}
273
+ </Button>
274
+ )
275
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Minimal MCP client for the docs page: one `tools/list` call over Streamable
3
+ * HTTP. Deliberately raw `fetch` rather than the MCP SDK — the page needs one
4
+ * unauthenticated read and nothing else.
5
+ *
6
+ * The endpoint is stateless (a fresh server per request), so `tools/list`
7
+ * stands alone with no `initialize` handshake. It may answer with either JSON
8
+ * or a single SSE event, so both are parsed.
9
+ */
10
+
11
+ export interface McpToolParameter {
12
+ name: string
13
+ type?: string
14
+ required: boolean
15
+ description?: string
16
+ enumValues?: string[]
17
+ }
18
+
19
+ export interface McpTool {
20
+ name: string
21
+ title?: string
22
+ description?: string
23
+ parameters: McpToolParameter[]
24
+ }
25
+
26
+ /** Path the MCP server is mounted at, relative to the site origin. */
27
+ export const MCP_ENDPOINT_PATH = '/api/mcp'
28
+
29
+ interface JsonSchemaProperty {
30
+ type?: string | string[]
31
+ description?: string
32
+ enum?: unknown[]
33
+ items?: { type?: string | string[]; enum?: unknown[] }
34
+ }
35
+
36
+ function describeType(prop: JsonSchemaProperty): string | undefined {
37
+ const base = Array.isArray(prop.type) ? prop.type.join(' | ') : prop.type
38
+ if (base === 'array') {
39
+ const itemType = Array.isArray(prop.items?.type)
40
+ ? prop.items.type.join(' | ')
41
+ : prop.items?.type
42
+ return itemType ? `${itemType}[]` : 'array'
43
+ }
44
+ return base
45
+ }
46
+
47
+ function enumValues(prop: JsonSchemaProperty): string[] | undefined {
48
+ const values = prop.enum ?? prop.items?.enum
49
+ return values?.length ? values.map(String) : undefined
50
+ }
51
+
52
+ /** Pull the single JSON-RPC payload out of a JSON or SSE response body. */
53
+ function parseBody(contentType: string, body: string): unknown {
54
+ if (!contentType.includes('text/event-stream')) {
55
+ return JSON.parse(body)
56
+ }
57
+ const dataLines = body
58
+ .split(/\r?\n/)
59
+ .filter((line) => line.startsWith('data:'))
60
+ .map((line) => line.slice('data:'.length).trim())
61
+ if (dataLines.length === 0) {
62
+ throw new Error('The server returned an empty event stream.')
63
+ }
64
+ return JSON.parse(dataLines.join(''))
65
+ }
66
+
67
+ /**
68
+ * Fetch the live tool registry so this page cannot go stale when the tools
69
+ * change.
70
+ *
71
+ * @param origin - Site origin, e.g. `window.location.origin`
72
+ * @param signal - Optional abort signal
73
+ */
74
+ export async function fetchMcpTools(
75
+ origin: string,
76
+ signal?: AbortSignal,
77
+ ): Promise<McpTool[]> {
78
+ const response = await fetch(`${origin}${MCP_ENDPOINT_PATH}`, {
79
+ method: 'POST',
80
+ signal,
81
+ headers: {
82
+ 'content-type': 'application/json',
83
+ // The Streamable HTTP transport requires the client to accept both.
84
+ accept: 'application/json, text/event-stream',
85
+ },
86
+ body: JSON.stringify({
87
+ jsonrpc: '2.0',
88
+ id: 1,
89
+ method: 'tools/list',
90
+ params: {},
91
+ }),
92
+ })
93
+
94
+ if (!response.ok) {
95
+ throw new Error(
96
+ `The MCP endpoint answered ${response.status} ${response.statusText}.`,
97
+ )
98
+ }
99
+
100
+ const payload = parseBody(
101
+ response.headers.get('content-type') ?? '',
102
+ await response.text(),
103
+ ) as {
104
+ error?: { message?: string }
105
+ result?: {
106
+ tools?: {
107
+ name: string
108
+ title?: string
109
+ description?: string
110
+ inputSchema?: {
111
+ properties?: Record<string, JsonSchemaProperty>
112
+ required?: string[]
113
+ }
114
+ }[]
115
+ }
116
+ }
117
+
118
+ if (payload.error) {
119
+ throw new Error(
120
+ payload.error.message ?? 'The MCP endpoint returned an error.',
121
+ )
122
+ }
123
+
124
+ return (payload.result?.tools ?? []).map((tool) => {
125
+ const properties = tool.inputSchema?.properties ?? {}
126
+ const required = new Set(tool.inputSchema?.required ?? [])
127
+ return {
128
+ name: tool.name,
129
+ title: tool.title,
130
+ description: tool.description,
131
+ parameters: Object.entries(properties).map(([name, prop]) => ({
132
+ name,
133
+ type: describeType(prop),
134
+ required: required.has(name),
135
+ description: prop.description,
136
+ enumValues: enumValues(prop),
137
+ })),
138
+ }
139
+ })
140
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Render-side helper for the search UI. The ranking engine itself lives in
3
+ * `@igstack/app-catalog-shared-core` so the UI and the MCP server rank
4
+ * identically and cannot drift.
5
+ */
6
+
7
+ /**
8
+ * Highlight matching text in a string
9
+ * @param text - Text to highlight
10
+ * @param query - Search query
11
+ * @returns Array of text segments with highlight flags
12
+ */
13
+ export function highlightText(
14
+ text: string,
15
+ query: string,
16
+ ): { text: string; highlight: boolean }[] {
17
+ if (!query.trim()) {
18
+ return [{ text, highlight: false }]
19
+ }
20
+
21
+ const normalizedQuery = query.trim().toLowerCase()
22
+ const lowerText = text.toLowerCase()
23
+ const index = lowerText.indexOf(normalizedQuery)
24
+
25
+ if (index === -1) {
26
+ return [{ text, highlight: false }]
27
+ }
28
+
29
+ const segments: { text: string; highlight: boolean }[] = []
30
+
31
+ // Text before match
32
+ if (index > 0) {
33
+ segments.push({ text: text.slice(0, index), highlight: false })
34
+ }
35
+
36
+ // Matched text
37
+ segments.push({
38
+ text: text.slice(index, index + normalizedQuery.length),
39
+ highlight: true,
40
+ })
41
+
42
+ // Text after match
43
+ if (index + normalizedQuery.length < text.length) {
44
+ segments.push({
45
+ text: text.slice(index + normalizedQuery.length),
46
+ highlight: false,
47
+ })
48
+ }
49
+
50
+ return segments
51
+ }
@@ -13,6 +13,7 @@ import { Route as LayoutRouteImport } from './routes/_layout'
13
13
  import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
14
14
  import { Route as AuthCallbackRouteImport } from './routes/auth.callback'
15
15
  import { Route as LayoutServiceDesksRouteImport } from './routes/_layout/service-desks'
16
+ import { Route as LayoutMcpRouteImport } from './routes/_layout/mcp'
16
17
  import { Route as LayoutLoginRouteImport } from './routes/_layout/login'
17
18
  import { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug'
18
19
  import { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'
@@ -37,6 +38,11 @@ const LayoutServiceDesksRoute = LayoutServiceDesksRouteImport.update({
37
38
  path: '/service-desks',
38
39
  getParentRoute: () => LayoutRoute,
39
40
  } as any)
41
+ const LayoutMcpRoute = LayoutMcpRouteImport.update({
42
+ id: '/mcp',
43
+ path: '/mcp',
44
+ getParentRoute: () => LayoutRoute,
45
+ } as any)
40
46
  const LayoutLoginRoute = LayoutLoginRouteImport.update({
41
47
  id: '/login',
42
48
  path: '/login',
@@ -61,6 +67,7 @@ const LayoutAppSlugSubSubSlugRoute = LayoutAppSlugSubSubSlugRouteImport.update({
61
67
  export interface FileRoutesByFullPath {
62
68
  '/': typeof LayoutIndexRoute
63
69
  '/login': typeof LayoutLoginRoute
70
+ '/mcp': typeof LayoutMcpRoute
64
71
  '/service-desks': typeof LayoutServiceDesksRoute
65
72
  '/auth/callback': typeof AuthCallbackRoute
66
73
  '/app/$slug': typeof LayoutAppSlugRoute
@@ -69,6 +76,7 @@ export interface FileRoutesByFullPath {
69
76
  }
70
77
  export interface FileRoutesByTo {
71
78
  '/login': typeof LayoutLoginRoute
79
+ '/mcp': typeof LayoutMcpRoute
72
80
  '/service-desks': typeof LayoutServiceDesksRoute
73
81
  '/auth/callback': typeof AuthCallbackRoute
74
82
  '/': typeof LayoutIndexRoute
@@ -80,6 +88,7 @@ export interface FileRoutesById {
80
88
  __root__: typeof rootRouteImport
81
89
  '/_layout': typeof LayoutRouteWithChildren
82
90
  '/_layout/login': typeof LayoutLoginRoute
91
+ '/_layout/mcp': typeof LayoutMcpRoute
83
92
  '/_layout/service-desks': typeof LayoutServiceDesksRoute
84
93
  '/auth/callback': typeof AuthCallbackRoute
85
94
  '/_layout/': typeof LayoutIndexRoute
@@ -92,6 +101,7 @@ export interface FileRouteTypes {
92
101
  fullPaths:
93
102
  | '/'
94
103
  | '/login'
104
+ | '/mcp'
95
105
  | '/service-desks'
96
106
  | '/auth/callback'
97
107
  | '/app/$slug'
@@ -100,6 +110,7 @@ export interface FileRouteTypes {
100
110
  fileRoutesByTo: FileRoutesByTo
101
111
  to:
102
112
  | '/login'
113
+ | '/mcp'
103
114
  | '/service-desks'
104
115
  | '/auth/callback'
105
116
  | '/'
@@ -110,6 +121,7 @@ export interface FileRouteTypes {
110
121
  | '__root__'
111
122
  | '/_layout'
112
123
  | '/_layout/login'
124
+ | '/_layout/mcp'
113
125
  | '/_layout/service-desks'
114
126
  | '/auth/callback'
115
127
  | '/_layout/'
@@ -153,6 +165,13 @@ declare module '@tanstack/react-router' {
153
165
  preLoaderRoute: typeof LayoutServiceDesksRouteImport
154
166
  parentRoute: typeof LayoutRoute
155
167
  }
168
+ '/_layout/mcp': {
169
+ id: '/_layout/mcp'
170
+ path: '/mcp'
171
+ fullPath: '/mcp'
172
+ preLoaderRoute: typeof LayoutMcpRouteImport
173
+ parentRoute: typeof LayoutRoute
174
+ }
156
175
  '/_layout/login': {
157
176
  id: '/_layout/login'
158
177
  path: '/login'
@@ -186,6 +205,7 @@ declare module '@tanstack/react-router' {
186
205
 
187
206
  interface LayoutRouteChildren {
188
207
  LayoutLoginRoute: typeof LayoutLoginRoute
208
+ LayoutMcpRoute: typeof LayoutMcpRoute
189
209
  LayoutServiceDesksRoute: typeof LayoutServiceDesksRoute
190
210
  LayoutIndexRoute: typeof LayoutIndexRoute
191
211
  LayoutAppSlugRoute: typeof LayoutAppSlugRoute
@@ -195,6 +215,7 @@ interface LayoutRouteChildren {
195
215
 
196
216
  const LayoutRouteChildren: LayoutRouteChildren = {
197
217
  LayoutLoginRoute: LayoutLoginRoute,
218
+ LayoutMcpRoute: LayoutMcpRoute,
198
219
  LayoutServiceDesksRoute: LayoutServiceDesksRoute,
199
220
  LayoutIndexRoute: LayoutIndexRoute,
200
221
  LayoutAppSlugRoute: LayoutAppSlugRoute,
@@ -0,0 +1,22 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'
3
+ import { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'
4
+ import { McpPage } from '~/modules/appCatalog/ui/pages/McpPage'
5
+
6
+ export const Route = createFileRoute('/_layout/mcp')({
7
+ component: RouteComponent,
8
+ async loader() {
9
+ const appCatalogLoader = await appCatalogRouteLoader()
10
+ return { appCatalogLoader }
11
+ },
12
+ })
13
+
14
+ function RouteComponent() {
15
+ const { queryClient, trpcClient } = Route.useRouteContext()
16
+
17
+ return (
18
+ <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>
19
+ <McpPage />
20
+ </AppCatalogLayout>
21
+ )
22
+ }
@@ -1,49 +0,0 @@
1
- import { Resource } from '@igstack/app-catalog-backend-core';
2
- export interface SearchMatch {
3
- /** Field where the match occurred */
4
- field: 'displayName' | 'abbreviation' | 'nicknames' | 'slug' | 'tags' | 'teams' | 'description' | 'subResource';
5
- /** Type of match */
6
- type: 'exact' | 'prefix' | 'contains';
7
- }
8
- export interface SearchResult {
9
- app: Resource;
10
- match: SearchMatch;
11
- }
12
- /**
13
- * Search and sort resources by relevance with highlighting support.
14
- * Only root resources (no parentSlug) are scored and returned.
15
- * Child resources (with parentSlug) contribute to their parent's score.
16
- *
17
- * Priority order:
18
- * 0. Exact match in abbreviation
19
- * 1. Exact match in displayName
20
- * 2. Exact match in nickname
21
- * 3. Prefix match in abbreviation
22
- * 4. Prefix match in displayName
23
- * 5. Prefix match in nickname
24
- * 6. Exact match in tags
25
- * 7. Prefix match in tags
26
- * 8. Contains match in abbreviation
27
- * 9. Contains match in displayName
28
- * 10. Contains match in nickname
29
- * 11. Contains match in tags
30
- * 12. Teams
31
- * 13. Description
32
- *
33
- * @param resources - Array of all resources (root + children)
34
- * @param searchQuery - Search query string
35
- * @returns Filtered and sorted array of root resources
36
- */
37
- export declare function searchResources(resources: Resource[], searchQuery: string): Resource[];
38
- /**
39
- * Highlight matching text in a string
40
- * @param text - Text to highlight
41
- * @param query - Search query
42
- * @returns Array of text segments with highlight flags
43
- */
44
- export declare function highlightText(text: string, query: string): {
45
- text: string;
46
- highlight: boolean;
47
- }[];
48
- /** @deprecated Use searchResources instead */
49
- export declare const searchApps: typeof searchResources;