Skip to content

LSP & MCP Providers

llms.txt lists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/lsp-mcp/.
Add the LSP completion and hover providers and the MCP tools and resources I describe to the lexicon I name, with tool and resource names in the lexicon's namespace as the page shows.
Write `completions.test.ts` and `hover.test.ts` and tests for each tool, run `npx vitest run lexicons/<name>` and `chant dev check-lexicon lexicons/<name>`, and open a pull request.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.

A lexicon can add editor completions and hover through LSP (Language Server Protocol), and agent tools through MCP (Model Context Protocol).

Core’s LexiconIndex, lexiconCompletions and lexiconHover, in packages/core/src/lsp/lexicon-providers.ts, 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.

A lexicon can contribute MCP tools and resources for AI agents:

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 catches 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.

chant workspace graph --intent <path>#<symbol> follows a declaration through history instead of a line range (#3034). Core resolves symbols in TypeScript and JavaScript. A lexicon whose members hold files in another language can resolve them too, by returning resolvers from symbolResolvers() (#3313):

import type { LexiconPlugin, SymbolResolver } from "@intentius/chant";
const sqlResolver: SymbolResolver = {
language: "SQL",
extensions: [".sql"],
declarations(path, text) {
// Every declaration in the file: its dotted name, its kind and its 1-based lines.
return [{ qualified: "app.users", kind: "table", lines: { start: 2, end: 4 } }];
},
};
export const plugin: LexiconPlugin = {
// ...
symbolResolvers: () => [sqlResolver],
};

The intent walk asks for them only for a file core’s resolver does not read, and only from the lexicons the file’s member configures in its chant.config. The config is read without running it when it can be, so graph --intent loads your plugin but no project code. A bare name finds the one declaration whose dotted name ends with it, and a name that matches several is refused as ambiguous, as for TypeScript. declarations does no I/O and calls no network. chant drops a resolver whose extensions don’t start with a dot, and any declaration whose lines are not a 1-based range. When a member’s lexicon fails to load, the intent-symbol-unsupported message names it.

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

chant