searchsocket 0.6.3 → 0.7.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/cli.js CHANGED
@@ -12,7 +12,7 @@ import { Command, Option } from "commander";
12
12
  // package.json
13
13
  var package_default = {
14
14
  name: "searchsocket",
15
- version: "0.6.3",
15
+ version: "0.7.0",
16
16
  description: "Semantic site search and MCP retrieval for SvelteKit static sites",
17
17
  license: "MIT",
18
18
  author: "Greg Priday <greg@siteorigin.com>",
@@ -4494,45 +4494,20 @@ var SearchEngine = class _SearchEngine {
4494
4494
  function createServer(engine) {
4495
4495
  const server = new McpServer({
4496
4496
  name: "searchsocket-mcp",
4497
- version: "0.1.0"
4497
+ version: "0.2.0"
4498
4498
  });
4499
4499
  server.registerTool(
4500
4500
  "search",
4501
4501
  {
4502
- description: `Semantic site search powered by Upstash Search. Returns url, title, snippet, chunkText, score, and routeFile per result. chunkText contains the full raw chunk markdown. When groupBy is 'page' (default), each result includes a chunks array with section-level sub-results containing sectionTitle, headingPath, snippet, and score. Supports optional filters for structured metadata (e.g. {"version": 2, "deprecated": false}).`,
4502
+ description: "Searches indexed site content using semantic similarity. Returns ranked results with url, title, snippet, chunkText (full section markdown), score, and routeFile (source file path for editing). Each result includes the best-matching section; set groupBy to 'page' (default) for additional chunk sub-results per page. Use routeFile to locate the source file when editing content. If snippets lack detail, call get_page with the result URL to retrieve the full page markdown.",
4503
4503
  inputSchema: {
4504
- query: z3.string().min(1),
4505
- scope: z3.string().optional(),
4506
- topK: z3.number().int().positive().max(100).optional(),
4507
- pathPrefix: z3.string().optional(),
4508
- tags: z3.array(z3.string()).optional(),
4509
- filters: z3.record(z3.string(), z3.union([z3.string(), z3.number(), z3.boolean()])).optional(),
4510
- groupBy: z3.enum(["page", "chunk"]).optional(),
4511
- maxSubResults: z3.number().int().positive().max(20).optional()
4512
- },
4513
- outputSchema: {
4514
- q: z3.string(),
4515
- scope: z3.string(),
4516
- results: z3.array(z3.object({
4517
- url: z3.string(),
4518
- title: z3.string(),
4519
- sectionTitle: z3.string().optional(),
4520
- snippet: z3.string(),
4521
- score: z3.number(),
4522
- routeFile: z3.string(),
4523
- chunks: z3.array(z3.object({
4524
- sectionTitle: z3.string().optional(),
4525
- snippet: z3.string(),
4526
- headingPath: z3.array(z3.string()),
4527
- score: z3.number()
4528
- })).optional()
4529
- })),
4530
- meta: z3.object({
4531
- timingsMs: z3.object({
4532
- search: z3.number(),
4533
- total: z3.number()
4534
- })
4535
- })
4504
+ query: z3.string().min(1).describe("Search query. Use keywords or natural language, not full sentences."),
4505
+ topK: z3.number().int().positive().max(100).optional().describe("Number of results to return (default: 10, max: 100)"),
4506
+ pathPrefix: z3.string().optional().describe("Filter results to URLs starting with this prefix (e.g. '/docs')"),
4507
+ tags: z3.array(z3.string()).optional().describe("Filter results to pages matching all specified tags"),
4508
+ filters: z3.record(z3.string(), z3.union([z3.string(), z3.number(), z3.boolean()])).optional().describe('Filter by structured page metadata (e.g. {"version": 2})'),
4509
+ groupBy: z3.enum(["page", "chunk"]).optional().describe("'page' (default) groups chunks by page with sub-results; 'chunk' returns individual chunks"),
4510
+ scope: z3.string().optional()
4536
4511
  }
4537
4512
  },
4538
4513
  async (input) => {
@@ -4543,85 +4518,18 @@ function createServer(engine) {
4543
4518
  pathPrefix: input.pathPrefix,
4544
4519
  tags: input.tags,
4545
4520
  filters: input.filters,
4546
- groupBy: input.groupBy,
4547
- maxSubResults: input.maxSubResults
4548
- });
4549
- return {
4550
- content: [
4551
- {
4552
- type: "text",
4553
- text: JSON.stringify(result, null, 2)
4554
- }
4555
- ],
4556
- structuredContent: result
4557
- };
4558
- }
4559
- );
4560
- server.registerTool(
4561
- "get_page",
4562
- {
4563
- description: "Fetch indexed markdown for a specific path or URL, including frontmatter and routeFile mapping.",
4564
- inputSchema: {
4565
- pathOrUrl: z3.string().min(1),
4566
- scope: z3.string().optional()
4567
- }
4568
- },
4569
- async (input) => {
4570
- const page = await engine.getPage(input.pathOrUrl, input.scope);
4571
- return {
4572
- content: [
4573
- {
4574
- type: "text",
4575
- text: JSON.stringify(page, null, 2)
4576
- }
4577
- ]
4578
- };
4579
- }
4580
- );
4581
- server.registerTool(
4582
- "list_pages",
4583
- {
4584
- description: "List indexed pages with optional path prefix filtering and cursor-based pagination. Returns url, title, description, and routeFile for each page. Use nextCursor to fetch subsequent pages.",
4585
- inputSchema: {
4586
- pathPrefix: z3.string().optional(),
4587
- cursor: z3.string().optional(),
4588
- limit: z3.number().int().positive().max(200).optional(),
4589
- scope: z3.string().optional()
4590
- }
4591
- },
4592
- async (input) => {
4593
- const result = await engine.listPages({
4594
- pathPrefix: input.pathPrefix,
4595
- cursor: input.cursor,
4596
- limit: input.limit,
4597
- scope: input.scope
4521
+ groupBy: input.groupBy
4598
4522
  });
4599
- return {
4600
- content: [
4601
- {
4602
- type: "text",
4603
- text: JSON.stringify(result, null, 2)
4604
- }
4605
- ]
4606
- };
4607
- }
4608
- );
4609
- server.registerTool(
4610
- "get_site_structure",
4611
- {
4612
- description: "Returns the hierarchical page tree derived from URL paths. Use this to understand site navigation structure, find where pages belong, or scope further operations to a section. Nodes with isIndexed: false are implicit structural parents not directly in the index. Large sites (>2000 pages) return truncated: true.",
4613
- inputSchema: {
4614
- pathPrefix: z3.string().optional(),
4615
- scope: z3.string().optional(),
4616
- maxPages: z3.number().int().positive().max(2e3).optional()
4523
+ if (result.results.length === 0) {
4524
+ return {
4525
+ content: [
4526
+ {
4527
+ type: "text",
4528
+ text: `No results found for "${input.query}". Try broader keywords or remove filters.`
4529
+ }
4530
+ ]
4531
+ };
4617
4532
  }
4618
- },
4619
- async (input) => {
4620
- const result = await engine.getSiteStructure({
4621
- pathPrefix: input.pathPrefix,
4622
- scope: input.scope,
4623
- maxPages: input.maxPages
4624
- });
4625
4533
  return {
4626
4534
  content: [
4627
4535
  {
@@ -4633,56 +4541,51 @@ function createServer(engine) {
4633
4541
  }
4634
4542
  );
4635
4543
  server.registerTool(
4636
- "find_source_file",
4544
+ "get_page",
4637
4545
  {
4638
- description: "Find the SvelteKit source file for a piece of site content. Use this when you need to locate and edit content on the site. Returns the URL, route file path, section title, and a content snippet.",
4546
+ description: "Retrieves the full markdown content and metadata for a specific page by its URL path. Use this after search when snippets lack the detail needed to answer a question. Returns reconstructed page markdown, frontmatter (title, routeFile, tags, link counts, indexedAt), and the source file path. Do NOT use this for discovery \u2014 use search first to find relevant pages.",
4639
4547
  inputSchema: {
4640
- query: z3.string().min(1),
4548
+ path: z3.string().min(1).describe("URL path of the page (e.g. '/docs/auth'). Use a URL from search results."),
4641
4549
  scope: z3.string().optional()
4642
4550
  }
4643
4551
  },
4644
4552
  async (input) => {
4645
- const result = await engine.search({
4646
- q: input.query,
4647
- topK: 1,
4648
- scope: input.scope
4649
- });
4650
- if (result.results.length === 0) {
4553
+ try {
4554
+ const page = await engine.getPage(input.path, input.scope);
4555
+ return {
4556
+ content: [
4557
+ {
4558
+ type: "text",
4559
+ text: JSON.stringify(page, null, 2)
4560
+ }
4561
+ ]
4562
+ };
4563
+ } catch {
4564
+ const suggestions = await engine.search({ q: input.path, topK: 3, scope: input.scope });
4565
+ const similar = suggestions.results.map((r) => r.url);
4651
4566
  return {
4652
4567
  content: [
4653
4568
  {
4654
4569
  type: "text",
4655
- text: JSON.stringify({
4656
- error: "No matching content found for the given query."
4657
- })
4570
+ text: similar.length > 0 ? `Page '${input.path}' not found. Similar pages: ${similar.join(", ")}` : `Page '${input.path}' not found. Use search to find the correct URL.`
4658
4571
  }
4659
4572
  ]
4660
4573
  };
4661
4574
  }
4662
- const match = result.results[0];
4663
- const { url, routeFile, sectionTitle, snippet } = match;
4664
- return {
4665
- content: [
4666
- {
4667
- type: "text",
4668
- text: JSON.stringify({ url, routeFile, sectionTitle, snippet })
4669
- }
4670
- ]
4671
- };
4672
4575
  }
4673
4576
  );
4674
4577
  server.registerTool(
4675
4578
  "get_related_pages",
4676
4579
  {
4677
- description: "Find pages related to a given URL using link graph, semantic similarity, and structural proximity. Returns related pages ranked by a composite relatedness score. Use this to discover content connected to a known page.",
4580
+ description: "Finds pages related to a specific page using link graph analysis, semantic similarity, and URL structure. Returns related pages with relationship type (outgoing_link, incoming_link, sibling, semantic) and relevance score. Do NOT use this for general search \u2014 use search instead. Use this only when you already have a specific page URL and need to discover connected content.",
4678
4581
  inputSchema: {
4679
- pathOrUrl: z3.string().min(1),
4680
- scope: z3.string().optional(),
4681
- topK: z3.number().int().positive().max(25).optional()
4582
+ path: z3.string().min(1).describe("URL path of the source page (e.g. '/docs/auth'). Use a URL from search results."),
4583
+ topK: z3.number().int().positive().max(25).optional().describe("Number of related pages to return (default: 10, max: 25)"),
4584
+ scope: z3.string().optional()
4682
4585
  }
4683
4586
  },
4684
4587
  async (input) => {
4685
- const result = await engine.getRelatedPages(input.pathOrUrl, {
4588
+ const result = await engine.getRelatedPages(input.path, {
4686
4589
  topK: input.topK,
4687
4590
  scope: input.scope
4688
4591
  });
package/dist/index.cjs CHANGED
@@ -21739,45 +21739,20 @@ var SearchEngine = class _SearchEngine {
21739
21739
  function createServer(engine) {
21740
21740
  const server = new mcp_js.McpServer({
21741
21741
  name: "searchsocket-mcp",
21742
- version: "0.1.0"
21742
+ version: "0.2.0"
21743
21743
  });
21744
21744
  server.registerTool(
21745
21745
  "search",
21746
21746
  {
21747
- description: `Semantic site search powered by Upstash Search. Returns url, title, snippet, chunkText, score, and routeFile per result. chunkText contains the full raw chunk markdown. When groupBy is 'page' (default), each result includes a chunks array with section-level sub-results containing sectionTitle, headingPath, snippet, and score. Supports optional filters for structured metadata (e.g. {"version": 2, "deprecated": false}).`,
21747
+ description: "Searches indexed site content using semantic similarity. Returns ranked results with url, title, snippet, chunkText (full section markdown), score, and routeFile (source file path for editing). Each result includes the best-matching section; set groupBy to 'page' (default) for additional chunk sub-results per page. Use routeFile to locate the source file when editing content. If snippets lack detail, call get_page with the result URL to retrieve the full page markdown.",
21748
21748
  inputSchema: {
21749
- query: zod.z.string().min(1),
21750
- scope: zod.z.string().optional(),
21751
- topK: zod.z.number().int().positive().max(100).optional(),
21752
- pathPrefix: zod.z.string().optional(),
21753
- tags: zod.z.array(zod.z.string()).optional(),
21754
- filters: zod.z.record(zod.z.string(), zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()])).optional(),
21755
- groupBy: zod.z.enum(["page", "chunk"]).optional(),
21756
- maxSubResults: zod.z.number().int().positive().max(20).optional()
21757
- },
21758
- outputSchema: {
21759
- q: zod.z.string(),
21760
- scope: zod.z.string(),
21761
- results: zod.z.array(zod.z.object({
21762
- url: zod.z.string(),
21763
- title: zod.z.string(),
21764
- sectionTitle: zod.z.string().optional(),
21765
- snippet: zod.z.string(),
21766
- score: zod.z.number(),
21767
- routeFile: zod.z.string(),
21768
- chunks: zod.z.array(zod.z.object({
21769
- sectionTitle: zod.z.string().optional(),
21770
- snippet: zod.z.string(),
21771
- headingPath: zod.z.array(zod.z.string()),
21772
- score: zod.z.number()
21773
- })).optional()
21774
- })),
21775
- meta: zod.z.object({
21776
- timingsMs: zod.z.object({
21777
- search: zod.z.number(),
21778
- total: zod.z.number()
21779
- })
21780
- })
21749
+ query: zod.z.string().min(1).describe("Search query. Use keywords or natural language, not full sentences."),
21750
+ topK: zod.z.number().int().positive().max(100).optional().describe("Number of results to return (default: 10, max: 100)"),
21751
+ pathPrefix: zod.z.string().optional().describe("Filter results to URLs starting with this prefix (e.g. '/docs')"),
21752
+ tags: zod.z.array(zod.z.string()).optional().describe("Filter results to pages matching all specified tags"),
21753
+ filters: zod.z.record(zod.z.string(), zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()])).optional().describe('Filter by structured page metadata (e.g. {"version": 2})'),
21754
+ groupBy: zod.z.enum(["page", "chunk"]).optional().describe("'page' (default) groups chunks by page with sub-results; 'chunk' returns individual chunks"),
21755
+ scope: zod.z.string().optional()
21781
21756
  }
21782
21757
  },
21783
21758
  async (input) => {
@@ -21788,85 +21763,18 @@ function createServer(engine) {
21788
21763
  pathPrefix: input.pathPrefix,
21789
21764
  tags: input.tags,
21790
21765
  filters: input.filters,
21791
- groupBy: input.groupBy,
21792
- maxSubResults: input.maxSubResults
21793
- });
21794
- return {
21795
- content: [
21796
- {
21797
- type: "text",
21798
- text: JSON.stringify(result, null, 2)
21799
- }
21800
- ],
21801
- structuredContent: result
21802
- };
21803
- }
21804
- );
21805
- server.registerTool(
21806
- "get_page",
21807
- {
21808
- description: "Fetch indexed markdown for a specific path or URL, including frontmatter and routeFile mapping.",
21809
- inputSchema: {
21810
- pathOrUrl: zod.z.string().min(1),
21811
- scope: zod.z.string().optional()
21812
- }
21813
- },
21814
- async (input) => {
21815
- const page = await engine.getPage(input.pathOrUrl, input.scope);
21816
- return {
21817
- content: [
21818
- {
21819
- type: "text",
21820
- text: JSON.stringify(page, null, 2)
21821
- }
21822
- ]
21823
- };
21824
- }
21825
- );
21826
- server.registerTool(
21827
- "list_pages",
21828
- {
21829
- description: "List indexed pages with optional path prefix filtering and cursor-based pagination. Returns url, title, description, and routeFile for each page. Use nextCursor to fetch subsequent pages.",
21830
- inputSchema: {
21831
- pathPrefix: zod.z.string().optional(),
21832
- cursor: zod.z.string().optional(),
21833
- limit: zod.z.number().int().positive().max(200).optional(),
21834
- scope: zod.z.string().optional()
21835
- }
21836
- },
21837
- async (input) => {
21838
- const result = await engine.listPages({
21839
- pathPrefix: input.pathPrefix,
21840
- cursor: input.cursor,
21841
- limit: input.limit,
21842
- scope: input.scope
21766
+ groupBy: input.groupBy
21843
21767
  });
21844
- return {
21845
- content: [
21846
- {
21847
- type: "text",
21848
- text: JSON.stringify(result, null, 2)
21849
- }
21850
- ]
21851
- };
21852
- }
21853
- );
21854
- server.registerTool(
21855
- "get_site_structure",
21856
- {
21857
- description: "Returns the hierarchical page tree derived from URL paths. Use this to understand site navigation structure, find where pages belong, or scope further operations to a section. Nodes with isIndexed: false are implicit structural parents not directly in the index. Large sites (>2000 pages) return truncated: true.",
21858
- inputSchema: {
21859
- pathPrefix: zod.z.string().optional(),
21860
- scope: zod.z.string().optional(),
21861
- maxPages: zod.z.number().int().positive().max(2e3).optional()
21768
+ if (result.results.length === 0) {
21769
+ return {
21770
+ content: [
21771
+ {
21772
+ type: "text",
21773
+ text: `No results found for "${input.query}". Try broader keywords or remove filters.`
21774
+ }
21775
+ ]
21776
+ };
21862
21777
  }
21863
- },
21864
- async (input) => {
21865
- const result = await engine.getSiteStructure({
21866
- pathPrefix: input.pathPrefix,
21867
- scope: input.scope,
21868
- maxPages: input.maxPages
21869
- });
21870
21778
  return {
21871
21779
  content: [
21872
21780
  {
@@ -21878,56 +21786,51 @@ function createServer(engine) {
21878
21786
  }
21879
21787
  );
21880
21788
  server.registerTool(
21881
- "find_source_file",
21789
+ "get_page",
21882
21790
  {
21883
- description: "Find the SvelteKit source file for a piece of site content. Use this when you need to locate and edit content on the site. Returns the URL, route file path, section title, and a content snippet.",
21791
+ description: "Retrieves the full markdown content and metadata for a specific page by its URL path. Use this after search when snippets lack the detail needed to answer a question. Returns reconstructed page markdown, frontmatter (title, routeFile, tags, link counts, indexedAt), and the source file path. Do NOT use this for discovery \u2014 use search first to find relevant pages.",
21884
21792
  inputSchema: {
21885
- query: zod.z.string().min(1),
21793
+ path: zod.z.string().min(1).describe("URL path of the page (e.g. '/docs/auth'). Use a URL from search results."),
21886
21794
  scope: zod.z.string().optional()
21887
21795
  }
21888
21796
  },
21889
21797
  async (input) => {
21890
- const result = await engine.search({
21891
- q: input.query,
21892
- topK: 1,
21893
- scope: input.scope
21894
- });
21895
- if (result.results.length === 0) {
21798
+ try {
21799
+ const page = await engine.getPage(input.path, input.scope);
21800
+ return {
21801
+ content: [
21802
+ {
21803
+ type: "text",
21804
+ text: JSON.stringify(page, null, 2)
21805
+ }
21806
+ ]
21807
+ };
21808
+ } catch {
21809
+ const suggestions = await engine.search({ q: input.path, topK: 3, scope: input.scope });
21810
+ const similar = suggestions.results.map((r) => r.url);
21896
21811
  return {
21897
21812
  content: [
21898
21813
  {
21899
21814
  type: "text",
21900
- text: JSON.stringify({
21901
- error: "No matching content found for the given query."
21902
- })
21815
+ text: similar.length > 0 ? `Page '${input.path}' not found. Similar pages: ${similar.join(", ")}` : `Page '${input.path}' not found. Use search to find the correct URL.`
21903
21816
  }
21904
21817
  ]
21905
21818
  };
21906
21819
  }
21907
- const match = result.results[0];
21908
- const { url, routeFile, sectionTitle, snippet } = match;
21909
- return {
21910
- content: [
21911
- {
21912
- type: "text",
21913
- text: JSON.stringify({ url, routeFile, sectionTitle, snippet })
21914
- }
21915
- ]
21916
- };
21917
21820
  }
21918
21821
  );
21919
21822
  server.registerTool(
21920
21823
  "get_related_pages",
21921
21824
  {
21922
- description: "Find pages related to a given URL using link graph, semantic similarity, and structural proximity. Returns related pages ranked by a composite relatedness score. Use this to discover content connected to a known page.",
21825
+ description: "Finds pages related to a specific page using link graph analysis, semantic similarity, and URL structure. Returns related pages with relationship type (outgoing_link, incoming_link, sibling, semantic) and relevance score. Do NOT use this for general search \u2014 use search instead. Use this only when you already have a specific page URL and need to discover connected content.",
21923
21826
  inputSchema: {
21924
- pathOrUrl: zod.z.string().min(1),
21925
- scope: zod.z.string().optional(),
21926
- topK: zod.z.number().int().positive().max(25).optional()
21827
+ path: zod.z.string().min(1).describe("URL path of the source page (e.g. '/docs/auth'). Use a URL from search results."),
21828
+ topK: zod.z.number().int().positive().max(25).optional().describe("Number of related pages to return (default: 10, max: 25)"),
21829
+ scope: zod.z.string().optional()
21927
21830
  }
21928
21831
  },
21929
21832
  async (input) => {
21930
- const result = await engine.getRelatedPages(input.pathOrUrl, {
21833
+ const result = await engine.getRelatedPages(input.path, {
21931
21834
  topK: input.topK,
21932
21835
  scope: input.scope
21933
21836
  });
package/dist/index.js CHANGED
@@ -21727,45 +21727,20 @@ var SearchEngine = class _SearchEngine {
21727
21727
  function createServer(engine) {
21728
21728
  const server = new McpServer({
21729
21729
  name: "searchsocket-mcp",
21730
- version: "0.1.0"
21730
+ version: "0.2.0"
21731
21731
  });
21732
21732
  server.registerTool(
21733
21733
  "search",
21734
21734
  {
21735
- description: `Semantic site search powered by Upstash Search. Returns url, title, snippet, chunkText, score, and routeFile per result. chunkText contains the full raw chunk markdown. When groupBy is 'page' (default), each result includes a chunks array with section-level sub-results containing sectionTitle, headingPath, snippet, and score. Supports optional filters for structured metadata (e.g. {"version": 2, "deprecated": false}).`,
21735
+ description: "Searches indexed site content using semantic similarity. Returns ranked results with url, title, snippet, chunkText (full section markdown), score, and routeFile (source file path for editing). Each result includes the best-matching section; set groupBy to 'page' (default) for additional chunk sub-results per page. Use routeFile to locate the source file when editing content. If snippets lack detail, call get_page with the result URL to retrieve the full page markdown.",
21736
21736
  inputSchema: {
21737
- query: z.string().min(1),
21738
- scope: z.string().optional(),
21739
- topK: z.number().int().positive().max(100).optional(),
21740
- pathPrefix: z.string().optional(),
21741
- tags: z.array(z.string()).optional(),
21742
- filters: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
21743
- groupBy: z.enum(["page", "chunk"]).optional(),
21744
- maxSubResults: z.number().int().positive().max(20).optional()
21745
- },
21746
- outputSchema: {
21747
- q: z.string(),
21748
- scope: z.string(),
21749
- results: z.array(z.object({
21750
- url: z.string(),
21751
- title: z.string(),
21752
- sectionTitle: z.string().optional(),
21753
- snippet: z.string(),
21754
- score: z.number(),
21755
- routeFile: z.string(),
21756
- chunks: z.array(z.object({
21757
- sectionTitle: z.string().optional(),
21758
- snippet: z.string(),
21759
- headingPath: z.array(z.string()),
21760
- score: z.number()
21761
- })).optional()
21762
- })),
21763
- meta: z.object({
21764
- timingsMs: z.object({
21765
- search: z.number(),
21766
- total: z.number()
21767
- })
21768
- })
21737
+ query: z.string().min(1).describe("Search query. Use keywords or natural language, not full sentences."),
21738
+ topK: z.number().int().positive().max(100).optional().describe("Number of results to return (default: 10, max: 100)"),
21739
+ pathPrefix: z.string().optional().describe("Filter results to URLs starting with this prefix (e.g. '/docs')"),
21740
+ tags: z.array(z.string()).optional().describe("Filter results to pages matching all specified tags"),
21741
+ filters: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe('Filter by structured page metadata (e.g. {"version": 2})'),
21742
+ groupBy: z.enum(["page", "chunk"]).optional().describe("'page' (default) groups chunks by page with sub-results; 'chunk' returns individual chunks"),
21743
+ scope: z.string().optional()
21769
21744
  }
21770
21745
  },
21771
21746
  async (input) => {
@@ -21776,85 +21751,18 @@ function createServer(engine) {
21776
21751
  pathPrefix: input.pathPrefix,
21777
21752
  tags: input.tags,
21778
21753
  filters: input.filters,
21779
- groupBy: input.groupBy,
21780
- maxSubResults: input.maxSubResults
21781
- });
21782
- return {
21783
- content: [
21784
- {
21785
- type: "text",
21786
- text: JSON.stringify(result, null, 2)
21787
- }
21788
- ],
21789
- structuredContent: result
21790
- };
21791
- }
21792
- );
21793
- server.registerTool(
21794
- "get_page",
21795
- {
21796
- description: "Fetch indexed markdown for a specific path or URL, including frontmatter and routeFile mapping.",
21797
- inputSchema: {
21798
- pathOrUrl: z.string().min(1),
21799
- scope: z.string().optional()
21800
- }
21801
- },
21802
- async (input) => {
21803
- const page = await engine.getPage(input.pathOrUrl, input.scope);
21804
- return {
21805
- content: [
21806
- {
21807
- type: "text",
21808
- text: JSON.stringify(page, null, 2)
21809
- }
21810
- ]
21811
- };
21812
- }
21813
- );
21814
- server.registerTool(
21815
- "list_pages",
21816
- {
21817
- description: "List indexed pages with optional path prefix filtering and cursor-based pagination. Returns url, title, description, and routeFile for each page. Use nextCursor to fetch subsequent pages.",
21818
- inputSchema: {
21819
- pathPrefix: z.string().optional(),
21820
- cursor: z.string().optional(),
21821
- limit: z.number().int().positive().max(200).optional(),
21822
- scope: z.string().optional()
21823
- }
21824
- },
21825
- async (input) => {
21826
- const result = await engine.listPages({
21827
- pathPrefix: input.pathPrefix,
21828
- cursor: input.cursor,
21829
- limit: input.limit,
21830
- scope: input.scope
21754
+ groupBy: input.groupBy
21831
21755
  });
21832
- return {
21833
- content: [
21834
- {
21835
- type: "text",
21836
- text: JSON.stringify(result, null, 2)
21837
- }
21838
- ]
21839
- };
21840
- }
21841
- );
21842
- server.registerTool(
21843
- "get_site_structure",
21844
- {
21845
- description: "Returns the hierarchical page tree derived from URL paths. Use this to understand site navigation structure, find where pages belong, or scope further operations to a section. Nodes with isIndexed: false are implicit structural parents not directly in the index. Large sites (>2000 pages) return truncated: true.",
21846
- inputSchema: {
21847
- pathPrefix: z.string().optional(),
21848
- scope: z.string().optional(),
21849
- maxPages: z.number().int().positive().max(2e3).optional()
21756
+ if (result.results.length === 0) {
21757
+ return {
21758
+ content: [
21759
+ {
21760
+ type: "text",
21761
+ text: `No results found for "${input.query}". Try broader keywords or remove filters.`
21762
+ }
21763
+ ]
21764
+ };
21850
21765
  }
21851
- },
21852
- async (input) => {
21853
- const result = await engine.getSiteStructure({
21854
- pathPrefix: input.pathPrefix,
21855
- scope: input.scope,
21856
- maxPages: input.maxPages
21857
- });
21858
21766
  return {
21859
21767
  content: [
21860
21768
  {
@@ -21866,56 +21774,51 @@ function createServer(engine) {
21866
21774
  }
21867
21775
  );
21868
21776
  server.registerTool(
21869
- "find_source_file",
21777
+ "get_page",
21870
21778
  {
21871
- description: "Find the SvelteKit source file for a piece of site content. Use this when you need to locate and edit content on the site. Returns the URL, route file path, section title, and a content snippet.",
21779
+ description: "Retrieves the full markdown content and metadata for a specific page by its URL path. Use this after search when snippets lack the detail needed to answer a question. Returns reconstructed page markdown, frontmatter (title, routeFile, tags, link counts, indexedAt), and the source file path. Do NOT use this for discovery \u2014 use search first to find relevant pages.",
21872
21780
  inputSchema: {
21873
- query: z.string().min(1),
21781
+ path: z.string().min(1).describe("URL path of the page (e.g. '/docs/auth'). Use a URL from search results."),
21874
21782
  scope: z.string().optional()
21875
21783
  }
21876
21784
  },
21877
21785
  async (input) => {
21878
- const result = await engine.search({
21879
- q: input.query,
21880
- topK: 1,
21881
- scope: input.scope
21882
- });
21883
- if (result.results.length === 0) {
21786
+ try {
21787
+ const page = await engine.getPage(input.path, input.scope);
21788
+ return {
21789
+ content: [
21790
+ {
21791
+ type: "text",
21792
+ text: JSON.stringify(page, null, 2)
21793
+ }
21794
+ ]
21795
+ };
21796
+ } catch {
21797
+ const suggestions = await engine.search({ q: input.path, topK: 3, scope: input.scope });
21798
+ const similar = suggestions.results.map((r) => r.url);
21884
21799
  return {
21885
21800
  content: [
21886
21801
  {
21887
21802
  type: "text",
21888
- text: JSON.stringify({
21889
- error: "No matching content found for the given query."
21890
- })
21803
+ text: similar.length > 0 ? `Page '${input.path}' not found. Similar pages: ${similar.join(", ")}` : `Page '${input.path}' not found. Use search to find the correct URL.`
21891
21804
  }
21892
21805
  ]
21893
21806
  };
21894
21807
  }
21895
- const match = result.results[0];
21896
- const { url, routeFile, sectionTitle, snippet } = match;
21897
- return {
21898
- content: [
21899
- {
21900
- type: "text",
21901
- text: JSON.stringify({ url, routeFile, sectionTitle, snippet })
21902
- }
21903
- ]
21904
- };
21905
21808
  }
21906
21809
  );
21907
21810
  server.registerTool(
21908
21811
  "get_related_pages",
21909
21812
  {
21910
- description: "Find pages related to a given URL using link graph, semantic similarity, and structural proximity. Returns related pages ranked by a composite relatedness score. Use this to discover content connected to a known page.",
21813
+ description: "Finds pages related to a specific page using link graph analysis, semantic similarity, and URL structure. Returns related pages with relationship type (outgoing_link, incoming_link, sibling, semantic) and relevance score. Do NOT use this for general search \u2014 use search instead. Use this only when you already have a specific page URL and need to discover connected content.",
21911
21814
  inputSchema: {
21912
- pathOrUrl: z.string().min(1),
21913
- scope: z.string().optional(),
21914
- topK: z.number().int().positive().max(25).optional()
21815
+ path: z.string().min(1).describe("URL path of the source page (e.g. '/docs/auth'). Use a URL from search results."),
21816
+ topK: z.number().int().positive().max(25).optional().describe("Number of related pages to return (default: 10, max: 25)"),
21817
+ scope: z.string().optional()
21915
21818
  }
21916
21819
  },
21917
21820
  async (input) => {
21918
- const result = await engine.getRelatedPages(input.pathOrUrl, {
21821
+ const result = await engine.getRelatedPages(input.path, {
21919
21822
  topK: input.topK,
21920
21823
  scope: input.scope
21921
21824
  });
@@ -18715,45 +18715,20 @@ var SearchEngine = class _SearchEngine {
18715
18715
  function createServer(engine) {
18716
18716
  const server = new mcp_js.McpServer({
18717
18717
  name: "searchsocket-mcp",
18718
- version: "0.1.0"
18718
+ version: "0.2.0"
18719
18719
  });
18720
18720
  server.registerTool(
18721
18721
  "search",
18722
18722
  {
18723
- description: `Semantic site search powered by Upstash Search. Returns url, title, snippet, chunkText, score, and routeFile per result. chunkText contains the full raw chunk markdown. When groupBy is 'page' (default), each result includes a chunks array with section-level sub-results containing sectionTitle, headingPath, snippet, and score. Supports optional filters for structured metadata (e.g. {"version": 2, "deprecated": false}).`,
18723
+ description: "Searches indexed site content using semantic similarity. Returns ranked results with url, title, snippet, chunkText (full section markdown), score, and routeFile (source file path for editing). Each result includes the best-matching section; set groupBy to 'page' (default) for additional chunk sub-results per page. Use routeFile to locate the source file when editing content. If snippets lack detail, call get_page with the result URL to retrieve the full page markdown.",
18724
18724
  inputSchema: {
18725
- query: zod.z.string().min(1),
18726
- scope: zod.z.string().optional(),
18727
- topK: zod.z.number().int().positive().max(100).optional(),
18728
- pathPrefix: zod.z.string().optional(),
18729
- tags: zod.z.array(zod.z.string()).optional(),
18730
- filters: zod.z.record(zod.z.string(), zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()])).optional(),
18731
- groupBy: zod.z.enum(["page", "chunk"]).optional(),
18732
- maxSubResults: zod.z.number().int().positive().max(20).optional()
18733
- },
18734
- outputSchema: {
18735
- q: zod.z.string(),
18736
- scope: zod.z.string(),
18737
- results: zod.z.array(zod.z.object({
18738
- url: zod.z.string(),
18739
- title: zod.z.string(),
18740
- sectionTitle: zod.z.string().optional(),
18741
- snippet: zod.z.string(),
18742
- score: zod.z.number(),
18743
- routeFile: zod.z.string(),
18744
- chunks: zod.z.array(zod.z.object({
18745
- sectionTitle: zod.z.string().optional(),
18746
- snippet: zod.z.string(),
18747
- headingPath: zod.z.array(zod.z.string()),
18748
- score: zod.z.number()
18749
- })).optional()
18750
- })),
18751
- meta: zod.z.object({
18752
- timingsMs: zod.z.object({
18753
- search: zod.z.number(),
18754
- total: zod.z.number()
18755
- })
18756
- })
18725
+ query: zod.z.string().min(1).describe("Search query. Use keywords or natural language, not full sentences."),
18726
+ topK: zod.z.number().int().positive().max(100).optional().describe("Number of results to return (default: 10, max: 100)"),
18727
+ pathPrefix: zod.z.string().optional().describe("Filter results to URLs starting with this prefix (e.g. '/docs')"),
18728
+ tags: zod.z.array(zod.z.string()).optional().describe("Filter results to pages matching all specified tags"),
18729
+ filters: zod.z.record(zod.z.string(), zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()])).optional().describe('Filter by structured page metadata (e.g. {"version": 2})'),
18730
+ groupBy: zod.z.enum(["page", "chunk"]).optional().describe("'page' (default) groups chunks by page with sub-results; 'chunk' returns individual chunks"),
18731
+ scope: zod.z.string().optional()
18757
18732
  }
18758
18733
  },
18759
18734
  async (input) => {
@@ -18764,85 +18739,18 @@ function createServer(engine) {
18764
18739
  pathPrefix: input.pathPrefix,
18765
18740
  tags: input.tags,
18766
18741
  filters: input.filters,
18767
- groupBy: input.groupBy,
18768
- maxSubResults: input.maxSubResults
18769
- });
18770
- return {
18771
- content: [
18772
- {
18773
- type: "text",
18774
- text: JSON.stringify(result, null, 2)
18775
- }
18776
- ],
18777
- structuredContent: result
18778
- };
18779
- }
18780
- );
18781
- server.registerTool(
18782
- "get_page",
18783
- {
18784
- description: "Fetch indexed markdown for a specific path or URL, including frontmatter and routeFile mapping.",
18785
- inputSchema: {
18786
- pathOrUrl: zod.z.string().min(1),
18787
- scope: zod.z.string().optional()
18788
- }
18789
- },
18790
- async (input) => {
18791
- const page = await engine.getPage(input.pathOrUrl, input.scope);
18792
- return {
18793
- content: [
18794
- {
18795
- type: "text",
18796
- text: JSON.stringify(page, null, 2)
18797
- }
18798
- ]
18799
- };
18800
- }
18801
- );
18802
- server.registerTool(
18803
- "list_pages",
18804
- {
18805
- description: "List indexed pages with optional path prefix filtering and cursor-based pagination. Returns url, title, description, and routeFile for each page. Use nextCursor to fetch subsequent pages.",
18806
- inputSchema: {
18807
- pathPrefix: zod.z.string().optional(),
18808
- cursor: zod.z.string().optional(),
18809
- limit: zod.z.number().int().positive().max(200).optional(),
18810
- scope: zod.z.string().optional()
18811
- }
18812
- },
18813
- async (input) => {
18814
- const result = await engine.listPages({
18815
- pathPrefix: input.pathPrefix,
18816
- cursor: input.cursor,
18817
- limit: input.limit,
18818
- scope: input.scope
18742
+ groupBy: input.groupBy
18819
18743
  });
18820
- return {
18821
- content: [
18822
- {
18823
- type: "text",
18824
- text: JSON.stringify(result, null, 2)
18825
- }
18826
- ]
18827
- };
18828
- }
18829
- );
18830
- server.registerTool(
18831
- "get_site_structure",
18832
- {
18833
- description: "Returns the hierarchical page tree derived from URL paths. Use this to understand site navigation structure, find where pages belong, or scope further operations to a section. Nodes with isIndexed: false are implicit structural parents not directly in the index. Large sites (>2000 pages) return truncated: true.",
18834
- inputSchema: {
18835
- pathPrefix: zod.z.string().optional(),
18836
- scope: zod.z.string().optional(),
18837
- maxPages: zod.z.number().int().positive().max(2e3).optional()
18744
+ if (result.results.length === 0) {
18745
+ return {
18746
+ content: [
18747
+ {
18748
+ type: "text",
18749
+ text: `No results found for "${input.query}". Try broader keywords or remove filters.`
18750
+ }
18751
+ ]
18752
+ };
18838
18753
  }
18839
- },
18840
- async (input) => {
18841
- const result = await engine.getSiteStructure({
18842
- pathPrefix: input.pathPrefix,
18843
- scope: input.scope,
18844
- maxPages: input.maxPages
18845
- });
18846
18754
  return {
18847
18755
  content: [
18848
18756
  {
@@ -18854,56 +18762,51 @@ function createServer(engine) {
18854
18762
  }
18855
18763
  );
18856
18764
  server.registerTool(
18857
- "find_source_file",
18765
+ "get_page",
18858
18766
  {
18859
- description: "Find the SvelteKit source file for a piece of site content. Use this when you need to locate and edit content on the site. Returns the URL, route file path, section title, and a content snippet.",
18767
+ description: "Retrieves the full markdown content and metadata for a specific page by its URL path. Use this after search when snippets lack the detail needed to answer a question. Returns reconstructed page markdown, frontmatter (title, routeFile, tags, link counts, indexedAt), and the source file path. Do NOT use this for discovery \u2014 use search first to find relevant pages.",
18860
18768
  inputSchema: {
18861
- query: zod.z.string().min(1),
18769
+ path: zod.z.string().min(1).describe("URL path of the page (e.g. '/docs/auth'). Use a URL from search results."),
18862
18770
  scope: zod.z.string().optional()
18863
18771
  }
18864
18772
  },
18865
18773
  async (input) => {
18866
- const result = await engine.search({
18867
- q: input.query,
18868
- topK: 1,
18869
- scope: input.scope
18870
- });
18871
- if (result.results.length === 0) {
18774
+ try {
18775
+ const page = await engine.getPage(input.path, input.scope);
18776
+ return {
18777
+ content: [
18778
+ {
18779
+ type: "text",
18780
+ text: JSON.stringify(page, null, 2)
18781
+ }
18782
+ ]
18783
+ };
18784
+ } catch {
18785
+ const suggestions = await engine.search({ q: input.path, topK: 3, scope: input.scope });
18786
+ const similar = suggestions.results.map((r) => r.url);
18872
18787
  return {
18873
18788
  content: [
18874
18789
  {
18875
18790
  type: "text",
18876
- text: JSON.stringify({
18877
- error: "No matching content found for the given query."
18878
- })
18791
+ text: similar.length > 0 ? `Page '${input.path}' not found. Similar pages: ${similar.join(", ")}` : `Page '${input.path}' not found. Use search to find the correct URL.`
18879
18792
  }
18880
18793
  ]
18881
18794
  };
18882
18795
  }
18883
- const match = result.results[0];
18884
- const { url, routeFile, sectionTitle, snippet } = match;
18885
- return {
18886
- content: [
18887
- {
18888
- type: "text",
18889
- text: JSON.stringify({ url, routeFile, sectionTitle, snippet })
18890
- }
18891
- ]
18892
- };
18893
18796
  }
18894
18797
  );
18895
18798
  server.registerTool(
18896
18799
  "get_related_pages",
18897
18800
  {
18898
- description: "Find pages related to a given URL using link graph, semantic similarity, and structural proximity. Returns related pages ranked by a composite relatedness score. Use this to discover content connected to a known page.",
18801
+ description: "Finds pages related to a specific page using link graph analysis, semantic similarity, and URL structure. Returns related pages with relationship type (outgoing_link, incoming_link, sibling, semantic) and relevance score. Do NOT use this for general search \u2014 use search instead. Use this only when you already have a specific page URL and need to discover connected content.",
18899
18802
  inputSchema: {
18900
- pathOrUrl: zod.z.string().min(1),
18901
- scope: zod.z.string().optional(),
18902
- topK: zod.z.number().int().positive().max(25).optional()
18803
+ path: zod.z.string().min(1).describe("URL path of the source page (e.g. '/docs/auth'). Use a URL from search results."),
18804
+ topK: zod.z.number().int().positive().max(25).optional().describe("Number of related pages to return (default: 10, max: 25)"),
18805
+ scope: zod.z.string().optional()
18903
18806
  }
18904
18807
  },
18905
18808
  async (input) => {
18906
- const result = await engine.getRelatedPages(input.pathOrUrl, {
18809
+ const result = await engine.getRelatedPages(input.path, {
18907
18810
  topK: input.topK,
18908
18811
  scope: input.scope
18909
18812
  });
package/dist/sveltekit.js CHANGED
@@ -18703,45 +18703,20 @@ var SearchEngine = class _SearchEngine {
18703
18703
  function createServer(engine) {
18704
18704
  const server = new McpServer({
18705
18705
  name: "searchsocket-mcp",
18706
- version: "0.1.0"
18706
+ version: "0.2.0"
18707
18707
  });
18708
18708
  server.registerTool(
18709
18709
  "search",
18710
18710
  {
18711
- description: `Semantic site search powered by Upstash Search. Returns url, title, snippet, chunkText, score, and routeFile per result. chunkText contains the full raw chunk markdown. When groupBy is 'page' (default), each result includes a chunks array with section-level sub-results containing sectionTitle, headingPath, snippet, and score. Supports optional filters for structured metadata (e.g. {"version": 2, "deprecated": false}).`,
18711
+ description: "Searches indexed site content using semantic similarity. Returns ranked results with url, title, snippet, chunkText (full section markdown), score, and routeFile (source file path for editing). Each result includes the best-matching section; set groupBy to 'page' (default) for additional chunk sub-results per page. Use routeFile to locate the source file when editing content. If snippets lack detail, call get_page with the result URL to retrieve the full page markdown.",
18712
18712
  inputSchema: {
18713
- query: z.string().min(1),
18714
- scope: z.string().optional(),
18715
- topK: z.number().int().positive().max(100).optional(),
18716
- pathPrefix: z.string().optional(),
18717
- tags: z.array(z.string()).optional(),
18718
- filters: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
18719
- groupBy: z.enum(["page", "chunk"]).optional(),
18720
- maxSubResults: z.number().int().positive().max(20).optional()
18721
- },
18722
- outputSchema: {
18723
- q: z.string(),
18724
- scope: z.string(),
18725
- results: z.array(z.object({
18726
- url: z.string(),
18727
- title: z.string(),
18728
- sectionTitle: z.string().optional(),
18729
- snippet: z.string(),
18730
- score: z.number(),
18731
- routeFile: z.string(),
18732
- chunks: z.array(z.object({
18733
- sectionTitle: z.string().optional(),
18734
- snippet: z.string(),
18735
- headingPath: z.array(z.string()),
18736
- score: z.number()
18737
- })).optional()
18738
- })),
18739
- meta: z.object({
18740
- timingsMs: z.object({
18741
- search: z.number(),
18742
- total: z.number()
18743
- })
18744
- })
18713
+ query: z.string().min(1).describe("Search query. Use keywords or natural language, not full sentences."),
18714
+ topK: z.number().int().positive().max(100).optional().describe("Number of results to return (default: 10, max: 100)"),
18715
+ pathPrefix: z.string().optional().describe("Filter results to URLs starting with this prefix (e.g. '/docs')"),
18716
+ tags: z.array(z.string()).optional().describe("Filter results to pages matching all specified tags"),
18717
+ filters: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe('Filter by structured page metadata (e.g. {"version": 2})'),
18718
+ groupBy: z.enum(["page", "chunk"]).optional().describe("'page' (default) groups chunks by page with sub-results; 'chunk' returns individual chunks"),
18719
+ scope: z.string().optional()
18745
18720
  }
18746
18721
  },
18747
18722
  async (input) => {
@@ -18752,85 +18727,18 @@ function createServer(engine) {
18752
18727
  pathPrefix: input.pathPrefix,
18753
18728
  tags: input.tags,
18754
18729
  filters: input.filters,
18755
- groupBy: input.groupBy,
18756
- maxSubResults: input.maxSubResults
18757
- });
18758
- return {
18759
- content: [
18760
- {
18761
- type: "text",
18762
- text: JSON.stringify(result, null, 2)
18763
- }
18764
- ],
18765
- structuredContent: result
18766
- };
18767
- }
18768
- );
18769
- server.registerTool(
18770
- "get_page",
18771
- {
18772
- description: "Fetch indexed markdown for a specific path or URL, including frontmatter and routeFile mapping.",
18773
- inputSchema: {
18774
- pathOrUrl: z.string().min(1),
18775
- scope: z.string().optional()
18776
- }
18777
- },
18778
- async (input) => {
18779
- const page = await engine.getPage(input.pathOrUrl, input.scope);
18780
- return {
18781
- content: [
18782
- {
18783
- type: "text",
18784
- text: JSON.stringify(page, null, 2)
18785
- }
18786
- ]
18787
- };
18788
- }
18789
- );
18790
- server.registerTool(
18791
- "list_pages",
18792
- {
18793
- description: "List indexed pages with optional path prefix filtering and cursor-based pagination. Returns url, title, description, and routeFile for each page. Use nextCursor to fetch subsequent pages.",
18794
- inputSchema: {
18795
- pathPrefix: z.string().optional(),
18796
- cursor: z.string().optional(),
18797
- limit: z.number().int().positive().max(200).optional(),
18798
- scope: z.string().optional()
18799
- }
18800
- },
18801
- async (input) => {
18802
- const result = await engine.listPages({
18803
- pathPrefix: input.pathPrefix,
18804
- cursor: input.cursor,
18805
- limit: input.limit,
18806
- scope: input.scope
18730
+ groupBy: input.groupBy
18807
18731
  });
18808
- return {
18809
- content: [
18810
- {
18811
- type: "text",
18812
- text: JSON.stringify(result, null, 2)
18813
- }
18814
- ]
18815
- };
18816
- }
18817
- );
18818
- server.registerTool(
18819
- "get_site_structure",
18820
- {
18821
- description: "Returns the hierarchical page tree derived from URL paths. Use this to understand site navigation structure, find where pages belong, or scope further operations to a section. Nodes with isIndexed: false are implicit structural parents not directly in the index. Large sites (>2000 pages) return truncated: true.",
18822
- inputSchema: {
18823
- pathPrefix: z.string().optional(),
18824
- scope: z.string().optional(),
18825
- maxPages: z.number().int().positive().max(2e3).optional()
18732
+ if (result.results.length === 0) {
18733
+ return {
18734
+ content: [
18735
+ {
18736
+ type: "text",
18737
+ text: `No results found for "${input.query}". Try broader keywords or remove filters.`
18738
+ }
18739
+ ]
18740
+ };
18826
18741
  }
18827
- },
18828
- async (input) => {
18829
- const result = await engine.getSiteStructure({
18830
- pathPrefix: input.pathPrefix,
18831
- scope: input.scope,
18832
- maxPages: input.maxPages
18833
- });
18834
18742
  return {
18835
18743
  content: [
18836
18744
  {
@@ -18842,56 +18750,51 @@ function createServer(engine) {
18842
18750
  }
18843
18751
  );
18844
18752
  server.registerTool(
18845
- "find_source_file",
18753
+ "get_page",
18846
18754
  {
18847
- description: "Find the SvelteKit source file for a piece of site content. Use this when you need to locate and edit content on the site. Returns the URL, route file path, section title, and a content snippet.",
18755
+ description: "Retrieves the full markdown content and metadata for a specific page by its URL path. Use this after search when snippets lack the detail needed to answer a question. Returns reconstructed page markdown, frontmatter (title, routeFile, tags, link counts, indexedAt), and the source file path. Do NOT use this for discovery \u2014 use search first to find relevant pages.",
18848
18756
  inputSchema: {
18849
- query: z.string().min(1),
18757
+ path: z.string().min(1).describe("URL path of the page (e.g. '/docs/auth'). Use a URL from search results."),
18850
18758
  scope: z.string().optional()
18851
18759
  }
18852
18760
  },
18853
18761
  async (input) => {
18854
- const result = await engine.search({
18855
- q: input.query,
18856
- topK: 1,
18857
- scope: input.scope
18858
- });
18859
- if (result.results.length === 0) {
18762
+ try {
18763
+ const page = await engine.getPage(input.path, input.scope);
18764
+ return {
18765
+ content: [
18766
+ {
18767
+ type: "text",
18768
+ text: JSON.stringify(page, null, 2)
18769
+ }
18770
+ ]
18771
+ };
18772
+ } catch {
18773
+ const suggestions = await engine.search({ q: input.path, topK: 3, scope: input.scope });
18774
+ const similar = suggestions.results.map((r) => r.url);
18860
18775
  return {
18861
18776
  content: [
18862
18777
  {
18863
18778
  type: "text",
18864
- text: JSON.stringify({
18865
- error: "No matching content found for the given query."
18866
- })
18779
+ text: similar.length > 0 ? `Page '${input.path}' not found. Similar pages: ${similar.join(", ")}` : `Page '${input.path}' not found. Use search to find the correct URL.`
18867
18780
  }
18868
18781
  ]
18869
18782
  };
18870
18783
  }
18871
- const match = result.results[0];
18872
- const { url, routeFile, sectionTitle, snippet } = match;
18873
- return {
18874
- content: [
18875
- {
18876
- type: "text",
18877
- text: JSON.stringify({ url, routeFile, sectionTitle, snippet })
18878
- }
18879
- ]
18880
- };
18881
18784
  }
18882
18785
  );
18883
18786
  server.registerTool(
18884
18787
  "get_related_pages",
18885
18788
  {
18886
- description: "Find pages related to a given URL using link graph, semantic similarity, and structural proximity. Returns related pages ranked by a composite relatedness score. Use this to discover content connected to a known page.",
18789
+ description: "Finds pages related to a specific page using link graph analysis, semantic similarity, and URL structure. Returns related pages with relationship type (outgoing_link, incoming_link, sibling, semantic) and relevance score. Do NOT use this for general search \u2014 use search instead. Use this only when you already have a specific page URL and need to discover connected content.",
18887
18790
  inputSchema: {
18888
- pathOrUrl: z.string().min(1),
18889
- scope: z.string().optional(),
18890
- topK: z.number().int().positive().max(25).optional()
18791
+ path: z.string().min(1).describe("URL path of the source page (e.g. '/docs/auth'). Use a URL from search results."),
18792
+ topK: z.number().int().positive().max(25).optional().describe("Number of related pages to return (default: 10, max: 25)"),
18793
+ scope: z.string().optional()
18891
18794
  }
18892
18795
  },
18893
18796
  async (input) => {
18894
- const result = await engine.getRelatedPages(input.pathOrUrl, {
18797
+ const result = await engine.getRelatedPages(input.path, {
18895
18798
  topK: input.topK,
18896
18799
  scope: input.scope
18897
18800
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "searchsocket",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Semantic site search and MCP retrieval for SvelteKit static sites",
5
5
  "license": "MIT",
6
6
  "author": "Greg Priday <greg@siteorigin.com>",