@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.
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/Highlight.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/Highlight.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +11 -0
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/McpPage.d.ts +11 -0
- package/dist/esm/modules/appCatalog/ui/pages/McpPage.js +227 -0
- package/dist/esm/modules/appCatalog/ui/pages/McpPage.js.map +1 -0
- package/dist/esm/modules/appCatalog/utils/fetchMcpTools.d.ts +32 -0
- package/dist/esm/modules/appCatalog/utils/fetchMcpTools.js +79 -0
- package/dist/esm/modules/appCatalog/utils/fetchMcpTools.js.map +1 -0
- package/dist/esm/modules/appCatalog/utils/highlightText.d.ts +15 -0
- package/dist/esm/modules/appCatalog/utils/highlightText.js +30 -0
- package/dist/esm/modules/appCatalog/utils/highlightText.js.map +1 -0
- package/dist/esm/routeTree.gen.d.ts +18 -3
- package/dist/esm/routeTree.gen.js +9 -2
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/mcp.d.ts +3 -0
- package/dist/esm/routes/_layout/mcp.js +20 -0
- package/dist/esm/routes/_layout/mcp.js.map +1 -0
- package/package.json +5 -5
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +1 -1
- package/src/modules/appCatalog/ui/components/Highlight.tsx +1 -1
- package/src/modules/appCatalog/ui/components/ViewToggle.tsx +10 -1
- package/src/modules/appCatalog/ui/pages/McpPage.tsx +275 -0
- package/src/modules/appCatalog/utils/fetchMcpTools.ts +140 -0
- package/src/modules/appCatalog/utils/highlightText.ts +51 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/_layout/mcp.tsx +22 -0
- package/dist/esm/__tests__/modules/appCatalog/utils/searchApps.test.d.ts +0 -1
- package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +0 -49
- package/dist/esm/modules/appCatalog/utils/searchApps.js +0 -144
- package/dist/esm/modules/appCatalog/utils/searchApps.js.map +0 -1
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +0 -96
- 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
|
+
}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -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 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -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;
|