Skip to content

LSP & MCP Providers

Lexicons can provide editor integration through LSP (Language Server Protocol) and MCP (Model Context Protocol) contributions.

Core provides LexiconIndex, lexiconCompletions and lexiconHover in packages/core/src/lsp/lexicon-providers.ts. They are generic implementations that work with any lexicon’s registry data:

import { LexiconIndex, lexiconCompletions, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
// The generated registry JSON: Record<className, LexiconEntry>.
import lexiconData from "../generated/lexicon-my-lexicon.json";
const index = new LexiconIndex(lexiconData);
// In your completionProvider():
return lexiconCompletions(ctx, index, "My resource");
// In your hoverProvider():
return lexiconHover(ctx, index, myCustomHoverFormatter);

The two context types come from packages/core/src/lsp/types.ts:

TypeFields
CompletionContexturi, content, position, wordAtCursor, linePrefix
HoverContexturi, content, position, word, lineText

lexiconCompletions reads linePrefix and wordAtCursor and returns CompletionItem[]. lexiconHover reads word, looks it up in the index, and returns HoverInfo | undefined; its third argument is an optional (className, entry) => HoverInfo | undefined formatter.

The completionProvider() method on your plugin returns completion items for resource and property type names. Use lexiconCompletions to generate standard completions from your registry:

completionProvider(ctx) {
return lexiconCompletions(ctx, index, "K8s resource");
}

See lexicons/aws/src/lsp/completions.ts for the AWS completion provider.

The hoverProvider() method returns hover information when users mouse over resource type constructors. Use lexiconHover with an optional custom formatter:

hoverProvider(ctx) {
return lexiconHover(ctx, index, (className, entry) => {
return { contents: `**${className}** — \`${entry.resourceType}\`` };
});
}

See lexicons/aws/src/lsp/hover.ts for the AWS hover provider.

Lexicons can contribute MCP tools and resources for AI agent integration:

McpToolContribution and McpResourceContribution are defined in packages/core/src/mcp/types.ts.

import type { McpToolContribution } from "@intentius/chant/mcp/types";
mcpTools(): McpToolContribution[] {
return [
{
name: "k8s:lookup-resource",
description: "Look up a resource type definition",
inputSchema: {
type: "object" as const,
properties: { name: { type: "string" } },
required: ["name"],
},
async handler(params: Record<string, unknown>): Promise<unknown> {
return { resourceType: String(params.name) };
},
},
];
}

A tool’s handler takes Record<string, unknown> and returns Promise<unknown>.

import type { McpResourceContribution } from "@intentius/chant/mcp/types";
mcpResources(): McpResourceContribution[] {
return [
{
uri: "catalog",
name: "K8s Resource Catalog",
description: "All available Kubernetes resource types",
mimeType: "application/json",
async handler(): Promise<string> {
return JSON.stringify([]);
},
},
];
}

A resource’s handler takes no arguments and must return Promise<string>. Serialize before returning.

Core registers every contribution under the lexicon’s own namespace: a tool as <lexicon>:<verb>, a resource as chant://<lexicon>/<path>. That is what an agent sees and what the docs name, so k8s:lookup-resource above registers as exactly that, and catalog registers as chant://k8s/catalog.

Applying the namespace is idempotent (chant #1341). namespacedToolName and namespacedResourceUri in packages/core/src/cli/mcp/server.ts accept three authored forms and normalize them all to one registered name:

You writeRegisters as
diff<lexicon>:diff
<lexicon>:diff<lexicon>:diff
catalogchant://<lexicon>/catalog
<lexicon>:resource-catalogchant://<lexicon>/resource-catalog
chant://<lexicon>/catalogchant://<lexicon>/catalog

That is what lets the shared createDiffTool and createCatalogResource helpers write their own prefix without doubling it. What does not normalize is a URI naming a different lexicon: chant://other/x contributed by aws registers as the unusable chant://aws/chant://other/x. chant dev check-lexicon fails any contribution whose registered name is not a single well-formed identifier, which is exactly that case.

Most lexicons contribute a common pair, both from the shared helpers in lexicon-plugin-helpers.ts:

ContributionWhat it doesLexicons
<lexicon>:diffDiff built output against a previous build12
chant://<lexicon>/resource-catalogThe lexicon’s resource types as JSON12

Both come from packages/core/src/lexicon-plugin-helpers.ts, so contributing them is two lines:

import { createDiffTool, createCatalogResource } from "@intentius/chant/lexicon-plugin-helpers";
mcpTools() {
return [createDiffTool(mySerializer, "Diff built output for My Lexicon", "my-lexicon")];
},
mcpResources() {
return [
createCatalogResource(
import.meta.url, // locates src/generated/
"My Lexicon Resource Catalog",
"JSON list of all supported resource types",
"lexicon-my-lexicon.json", // the generated registry filename
"my-lexicon",
),
];
},

The same module exports createSkillsLoader, which the skills() member uses. See Skills.

The CI lexicons also ship a read-only context set, each tool answering from a local build without touching a live forge. gitlab and forgejo contribute all eight (checks, workflow or pipeline, references, affected, workflow-yaml or pipeline-yaml, source, owns, compare); github contributes the first five. A new CI lexicon should match those names rather than invent its own. See lexicons/gitlab/src/mcp/context-tools.ts.

Add skills for AI agent guidance, then package and publish your lexicon.