Skip to content
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ Use the `fpf_memory` MCP server whenever the task requires grounded answers, exa

Public tools (deployed MCP surface):

- `browse_fpf_catalog` for task-oriented discovery by part, status, or kind
- `search_fpf` for full-text search across compiled nodes
- `ask_fpf` for markdown-first answers
- `query_fpf_spec` for structured answer envelopes
- `read_fpf_doc` for exact generated markdown pages
- `get_fpf_index_status` for runtime freshness checks

Expert tools (local full-surface runtime only, via `FPF_MCP_SURFACE=full bun run mcp`):

- `read_fpf_doc` for exact generated markdown pages
- `trace_fpf_path` for retrieval evidence and provenance
- `inspect_fpf_node`, `inspect_fpf_anchor`, `expand_fpf_citations` for deep inspection
- `trace_fpf_path` for retrieval evidence and provenance

Admin tools (index management):

- `refresh_fpf_index` to rebuild the local artifact set
2 changes: 1 addition & 1 deletion src/mastra/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export const fpfMemory = new MCPServer({
export const fpfMemoryPublic = new MCPServer({
name: 'fpf_memory',
version: '1.0.0',
description: 'FPF-spec query runtime with public tool surface (ask, query, status).',
description: 'FPF-spec query runtime with public discovery surface (browse, search, ask, query, read, status).',
tools: fpfPublicTools,
});
84 changes: 84 additions & 0 deletions src/mcp/tool-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,87 @@ export type ReadFpfDocInput = z.infer<typeof readFpfDocInputSchema>;
export type InspectFpfAnchorInput = z.infer<typeof inspectFpfAnchorInputSchema>;
export type ExpandFpfCitationsInput = z.infer<typeof expandFpfCitationsInputSchema>;
export type TraceFpfPathInput = z.infer<typeof traceFpfPathInputSchema>;

// ---------------------------------------------------------------------------
// Discovery layer schemas (browse / search)
// ---------------------------------------------------------------------------

export const catalogEntrySchema = z
.object({
id: z.string(),
kind: nodeKindSchema,
title: z.string(),
status: z.string().optional(),
part: z.string().optional(),
cluster: z.string().optional(),
description: z.string(),
})
.strict();

export const browseFpfCatalogInputSchema = z
.object({
part: z.string().optional(),
status: z.string().optional(),
kind: nodeKindSchema.optional(),
limit: z.number().int().min(1).max(500).optional(),
forceRefresh: z.boolean().optional(),
})
.strict();
Comment on lines +505 to +513
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The browse_fpf_catalog tool should ideally include a limit parameter (similar to the search_fpf tool) to prevent returning an excessively large number of entries in a single response. As the specification grows, returning the entire catalog could impact performance or exceed MCP message size limits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — added limit parameter to browseFpfCatalogInputSchema (min 1, max 500) and the runtime browse() method now defaults to 200 entries. The total field still reports the full count of matched entries so callers know when they're seeing a subset.

Also applied filter-before-map optimization per the suggestion below — nodes are now filtered before calling nodeToCatalogEntry() to avoid unnecessary graph/lexicon lookups.


export const browseFpfCatalogResultSchema = z
.object({
entries: z.array(catalogEntrySchema),
total: z.number(),
filters: z
.object({
part: z.string().optional(),
status: z.string().optional(),
kind: nodeKindSchema.optional(),
})
.strict(),
snapshot: z
.object({
sourceHash: z.string(),
builtAt: z.string(),
})
.strict(),
})
.strict();

export const searchHitSchema = z
.object({
id: z.string(),
kind: nodeKindSchema,
title: z.string(),
status: z.string().optional(),
part: z.string().optional(),
score: z.number(),
snippet: z.string(),
})
.strict();

export const searchFpfInputSchema = z
.object({
query: z.string().min(1),
kind: nodeKindSchema.optional(),
limit: z.number().int().min(1).max(100).optional(),
forceRefresh: z.boolean().optional(),
})
.strict();

export const searchFpfResultSchema = z
.object({
query: z.string(),
hits: z.array(searchHitSchema),
total: z.number(),
snapshot: z
.object({
sourceHash: z.string(),
builtAt: z.string(),
})
.strict(),
})
.strict();

export type BrowseFpfCatalogInput = z.infer<typeof browseFpfCatalogInputSchema>;
export type SearchFpfInput = z.infer<typeof searchFpfInputSchema>;
49 changes: 45 additions & 4 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createTool } from '@mastra/core/tools';

import { FpfRuntime } from '../runtime/runtime.js';
import type { AnswerMode, AskFpfResult, QueryResult } from '../runtime/types.js';
import type { AnswerMode, AskFpfResult, NodeKind, QueryResult } from '../runtime/types.js';
import {
askFpfInputSchema,
askFpfResultSchema,
browseFpfCatalogInputSchema,
browseFpfCatalogResultSchema,
expandCitationsResultSchema,
expandFpfCitationsInputSchema,
getFpfIndexStatusInputSchema,
Expand All @@ -19,6 +21,8 @@ import {
refreshFpfIndexInputSchema,
runtimeStatusSchema,
buildAuditSchema,
searchFpfInputSchema,
searchFpfResultSchema,
traceFpfPathInputSchema,
traceResultSchema,
} from './tool-contracts.js';
Expand Down Expand Up @@ -121,27 +125,64 @@ export const traceFpfPathTool = createTool({
runtime.trace(question, mode ?? 'compact', forceRefresh ?? false, sessionId),
});

export const browseFpfCatalogTool = createTool({
id: 'browse_fpf_catalog',
description:
'Browse the FPF catalog of compiled patterns, routes, and lexicon entries. Filter by part, status, or kind to discover relevant material before drilling into individual nodes.',
inputSchema: browseFpfCatalogInputSchema,
outputSchema: browseFpfCatalogResultSchema,
execute: async ({ part, status, kind, limit, forceRefresh }) =>
runtime.browse({
part,
status,
kind: kind as NodeKind | undefined,
limit,
forceRefresh: forceRefresh ?? false,
}),
});

export const searchFpfTool = createTool({
id: 'search_fpf',
description:
'Full-text search across all compiled FPF nodes. Returns ranked hits with contextual snippets. Use this to find patterns, routes, or lexicon entries by keyword or concept.',
inputSchema: searchFpfInputSchema,
outputSchema: searchFpfResultSchema,
execute: async ({ query, kind, limit, forceRefresh }) =>
runtime.search(query, {
kind: kind as NodeKind | undefined,
limit,
forceRefresh: forceRefresh ?? false,
}),
});

/** Public tools — safe for deployed MCP surface. */
export const fpfPublicTools = {
browse_fpf_catalog: browseFpfCatalogTool,
search_fpf: searchFpfTool,
ask_fpf: askFpfTool,
query_fpf_spec: queryFpfSpecTool,
read_fpf_doc: readFpfDocTool,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
get_fpf_index_status: getFpfIndexStatusTool,
} as const;

/** Expert/debug tools — full-surface runtime only. */
export const fpfExpertTools = {
refresh_fpf_index: refreshFpfIndexTool,
trace_fpf_path: traceFpfPathTool,
inspect_fpf_node: inspectFpfNodeTool,
read_fpf_doc: readFpfDocTool,
inspect_fpf_anchor: inspectFpfAnchorTool,
expand_fpf_citations: expandFpfCitationsTool,
trace_fpf_path: traceFpfPathTool,
} as const;

/** Admin tools — index management. */
export const fpfAdminTools = {
refresh_fpf_index: refreshFpfIndexTool,
} as const;

/** All tools — used by the full-surface MCP runtime. */
export const fpfMcpTools = {
...fpfPublicTools,
...fpfExpertTools,
...fpfAdminTools,
} as const;

export function resolveDefaultQueryMode(env: NodeJS.ProcessEnv = process.env): AnswerMode {
Expand Down
Loading
Loading