Publishing a capability
You have an account. This is the rest: what you are publishing, how to lay it out, how to claim a name for it, and the two commands that put it on the registry.
What a capability is
A capability is one function. Not a library, not a class, not a service: one function with a name, a signature, and an answer you can write down. finance.tax.add-vat adds VAT to an amount. collections.paginate cuts a list into pages.
It is implemented three times — once in TypeScript, once in Python, once in Rust — and pinned by one set of test vectors that all three must pass. Someone using it names it in one line of their code; fune build turns that into readable source in their own language, in the project’s .fune/build folder, with a header saying where it came from, so they can open it, step through it and diff it — and fune check refuses a copy that has been edited.
That is what publishing puts your name to: the three implementations agree, and the vectors say what agreeing means. Everything below exists to keep that claim true.
The directory layout
One directory per version. Publish the version directory, not the capability directory — the id and the version both come out of function.fune, and a directory named after them keeps a whole registry readable.
registry/finance.tax.add-vat/1.0.0/
function.fune metadata, signature, contract
README.md notes for the people deciding whether to use it
vectors.json the shared test cases
impl/typescript.ts
impl/python.py
impl/rust.rs
data/vat-rules.json optional: rules that change by date or jurisdiction
The id is dotted lowercase with at least two segments. The first segment is the namespace, and it decides who may publish the name at all — see namespaces below.
Every file in the directory is uploaded, recursively, so keep build output, __pycache__ and editor leftovers out of it. A version may ship at most 64 files, and no single file may exceed 2MB.
function.fune
The contract. It is what the website renders, what the resolver reads, and what the integrity hash covers. It uses the same grammar as fune.project: one directive per line, # for comments, indented lines belong to the block above.
# collections.paginate
id collections.paginate
version 1.0.0
level 1
summary Cut a list into pages, returning the slice and the page count.
tags collections pagination api
throws yes
fn paginate<T>(items: T[], page: int, perPage: int) -> Page<T>
items the whole list
page 1-based
type Page<T> One page, and what a client needs to draw a pager.
items T[]
page int
totalPages int
The fn line is the signature every implementation must have, in one vocabulary with a fixed meaning in each language:
function paginate<T>(items: readonly T[], page: number, perPage: number): Page<T>
def paginate(items: Sequence[T], page: int, per_page: int) -> Page[T]
pub fn paginate<T>(items: &[T], page: i64, per_page: i64) -> Page<T>
Anything that follows the conventions is left out. The implementations are impl/typescript.ts, impl/python.py and impl/rust.rs, for whichever of them exist; the Python and Rust names are the snake_case of the ones in the manifest; a capability is pure, deterministic, off the network and does not throw unless it says otherwise.
| Line | What it has to be |
|---|---|
| id | Dotted lowercase, two segments or more. It never changes once published. |
| version | Semver. A new upload is always a new version. |
| level | 0 primitive, 1 generic concept, 2 domain function, 3 business operation, 4 workflow. Pick the lowest level that is honest. |
| summary | One line under 120 characters. It is the only thing most people read. |
| fn | The signature: the exported function as TypeScript names it, each parameter with its type, and -> the result. Indented lines under it are notes on a parameter, or on ->. A language whose entry breaks the snake_case convention says so on an entry line: entry python=add_vat_py. |
| type | A type the signature uses: a record with one indented field type [note] line per field, or a set of strings, type RoundingMode = half-up | down. It is generated into every language; implementations import it and never declare it themselves. |
| throws | Also pure, deterministic, network: yes or no, written only when it is not the default. A function that reads the clock or the network cannot be pinned by a vector. |
| require | Other capabilities, by id and range, as in fune.project. Nothing from npm, PyPI or crates.io: the standard library and the registry, and that is all. Every import must name a required capability and something it exports in each version the range allows, or the upload is refused. |
Types
A type is a built-in, one declared in the manifest, or one declared by a capability it requires directly. Each has one meaning per language; Rust borrows parameters and owns results.
| Manifest | TypeScript | Python | Rust |
|---|---|---|---|
| int | number | int | i64 |
| float | number | float | f64 |
| string | string | str | &str in, String out |
| bool | boolean | bool | bool |
| date | string | str | &str in, String out |
| json | unknown | Any | &Value in, Value out |
| record | Readonly<Record<string, unknown>> | Mapping[str, Any] | &Value in, Value out |
| map<int> | Readonly<Record<string, number>> | Mapping[str, int] in, Dict[str, int] out | &[(String, i64)] in, Vec<(String, i64)> out |
| Money | Money | Money | &Money in, Money out |
| Money[] | readonly Money[] | Sequence[Money] in, List[Money] out | &[Money] in, Vec<Money> out |
| string? | string | null | Optional[str] | Option<&str> in, Option<String> out |
| T | T | T | &T in, T out |
Explanations belong in README.md next to it, which is shown on the capability's page. The service checks all of this on upload with the same code fune validate runs on your machine, so a capability that validates locally is a capability that will be accepted. A package still on capability.json converts with fune migrate <dir>.
Vectors, and why all three languages must agree
A vector is a case: arguments in, expected value out, written in plain JSON so it belongs to no language in particular.
[
{
"name": "the first page of three",
"args": [[1, 2, 3, 4, 5], 1, 2],
"expect": { "items": [1, 2], "pages": 3 }
},
{
"name": "a page past the end is empty",
"args": [[1, 2], 9, 2],
"expect": { "items": [], "pages": 1 }
},
{
"name": "page zero is an error",
"args": [[1, 2], 0, 2],
"expectError": "page must be 1 or more"
}
]
The same file runs in all three implementations. fune verify --all installs your capability into a throwaway project per language and runs every case through the real toolchain — tsc, python, cargo. If TypeScript rounds a half up and Python rounds it to even, the run fails and you have found a bug that would otherwise have been someone's wrong invoice.
This is the whole point of the registry. A capability that is verified in one language is a snippet; one that answers identically in three is something you can build on in a codebase that is not all written in the same thing. So:
- Every function is tested. The registry refuses a version with fewer than eight cases for any function, and a function that throws needs at least one expectError case. Make them count: the ordinary case, the boundary, zero, a negative, the largest values, the rounding edge, and every error it documents.
- expectError matches on a substring of the message, in all three languages: the TypeScript error, the Python exception and the Rust panic, which the harness catches. Use the same wording in each.
- Vectors are the specification. If you find yourself editing a vector to match the code, work out which of the two is actually wrong.
Run both gates before you publish:
fune validate finance.tax.add-vat
fune verify --all finance.tax.add-vat
Groups: several functions in one package
Publish one function per capability unless you have a reason not to. A function that is useful on its own - adding VAT, rounding money - is its own package, found and versioned on its own. Only functions that are parts of one machine and no use apart, like a chart's scale, its inverse and a nice axis domain, belong together in a group: one manifest with a fn line for each.
fn linearScale(domain: float[], range: float[], value: float, clamp: bool) -> float
throws yes
fn invertLinear(domain: float[], range: float[], value: float) -> float
throws yes
fn niceDomain(domain: float[], count: int) -> NiceDomain
throws yes
impl/typescript/linear_scale.ts one file per function, per language
impl/typescript/invert_linear.ts import { normalise } from "./charts_scale_linear_scale.ts";
impl/typescript/nice_domain.ts
impl/python/… impl/rust/…
vectors.json { "fn": "linearScale", "name": …, "args": …, "expect": … }
- Each function has its own file in each language, named by its snake_case, and says for itself whether it throws. Types, requires and data are shared by the package.
- Each vector names the function it calls with "fn", and every function needs eight of its own, with an error case if it throws.
- A function may import a sibling's module, <id>_<function>, for what that file exports. The check is the same as for any import, and it is also how a slim install knows what a function needs.
- A project can install only some functions with fune add charts.scale --only invertLinear: it gets those, the siblings they import, and the types they use. The package you publish, and each language's .fune, always carries the whole group.
- Search finds every function by name, in every language's spelling, and the group's page gives each one its own anchor, signature and source.
An organisation, and a namespace to publish under
Names are not first come, first served. An organisation claims a dotted prefix — finance, or acme.billing — and from then on only its members can publish underneath it. The longest matching claim wins, and no prefix can be claimed inside another organisation's, so there is always exactly one answer to who owns a name.
Skip this and the upload is refused, with the fix in the message:
No organisation owns the “acme” namespace yet. Claim it from your organisation's settings, then publish.
So, once:
- Create an organisation from your settings. You become its owner.
- Claim the namespace your ids start with, in the organisation's settings.
- Add anyone else who will publish, with the publisher role or better.
Roles run reader, publisher, admin, owner. Publishing needs publisher; claiming and releasing namespaces needs admin. A namespace with published capabilities under it cannot be released, because that would leave them unowned.
Browse who owns what at http://www.functionalweave.com/orgs.
fune login
The CLI reads the registry without an account. Publishing needs a token, and fune login gets one. Not installed yet? Install the CLI first. By default it signs you in through the browser: it prints a short code and opens a page on this site where, signed in, you check the code and approve it. The token comes back to the terminal; your password never goes near it.
fune login --registry http://www.functionalweave.com
fune login --password asks for your username and password in the terminal instead (not echoed). Either way only the token that was issued is stored, in ~/.fune/credentials.json, keyed by registry and written readable by you alone. Against https://functionalweave.com, or inside a project whose fune.project already names this registry, or with FUNE_REGISTRY set, the flag is unnecessary:
fune login
fune whoami
fune logout
fune whoami prints who the stored token belongs to and which organisations you are in, with your role in each. fune logout forgets the token on this machine; to kill it everywhere, revoke it in your settings.
On a build machine, make a token on the website instead of typing a password into CI, and hand it over directly:
fune login --registry http://www.functionalweave.com --token "$FUNE_TOKEN"
Or leave it in the environment: with FUNE_TOKEN set, fune publish uses it without storing anything.
Either way the token is checked against the registry before it is written, so a typo fails here rather than three steps later.
fune publish
Point it at the version directory. It reads function.fune and every other file in there, validates the lot locally, and only then uploads.
fune publish ./registry/finance.tax.add-vat/1.1.0
Look before you leap. --dry-run does everything except the upload and says exactly what would go:
$ fune publish ./registry/finance.tax.add-vat/1.1.0 --dry-run
would publish finance.tax.add-vat @1.1.0 to http://www.functionalweave.com
integrity sha256-rxzv5LNCoqcL77aYY1Bp/FW3NW6mwaMdn4Ykb5AiP7k=
languages python, rust, typescript
vectors 22
files
function.fune 612 B
README.md 1,340 B
impl/python.py 3,043 B
impl/rust.rs 4,134 B
impl/typescript.ts 3,262 B
vectors.json 3,027 B
dry run: 6 files, 15,418 B, nothing uploaded
Validation happens on your machine first and reports every problem at once, because a round trip spent discovering a misspelled export is a round trip wasted. When something is wrong, nothing is uploaded:
invalid text.slugify@1.0.2
- missing "summary"
- python: "slugifyy" is not exported by impl/python.py
error 2 problems to fix before publishing
nothing was uploaded
The three refusals that come from the service, and what each one means:
| You see | It means |
|---|---|
| did not accept that token | No token, or a revoked one. Run fune login. |
| No organisation owns … | The namespace is unclaimed, or claimed by someone else, or you are not a publisher in the organisation that owns it. See namespaces. |
| is already published | That exact version exists. Bump the version and publish again. |
A successful publish tells you what landed:
published finance.tax.add-vat@1.1.0 31ms
integrity sha256-rxzv5LNCoqcL77aYY1Bp/FW3NW6mwaMdn4Ykb5AiP7k=
languages python, rust, typescript
vectors 22 pinned
published as acme
url http://www.functionalweave.com/functions/finance.tax.add-vat
package http://www.functionalweave.com/finance.tax.add-vat/1.1.0.fune sha256 5d0c…
fune publish also takes a .fune file made by fune pack in place of the directory — see packages. Either way the same checks run and the same version lands.
Packages: a version as one file
A directory is the right shape to write and review a capability in. To hand one over, a version is also a single file: a .fune package, named <id>-<version>.cap. It is a JSON document with the manifest, the vectors, every implementation and any data inside it, so you can open it and read the code before you trust it.
{
"format": "fune-package/1",
"integrity": "sha256-/DEyZA9fylYwsE80wsSfMw+D5OhxEgeolL9agTUscBg=",
"capability": { "id": "finance.tax.add-vat", "version": "1.0.0", … },
"files": {
"impl/python.py": "…the source, as text…",
"impl/rust.rs": "…",
"impl/typescript.ts": "…",
"vectors.json": "…"
}
}
Text files are stored as text; anything that is not UTF-8 is stored as { "base64": … }. Keys are sorted and there are no timestamps, so packing the same directory twice gives the same bytes, and the file this registry serves is byte for byte the file fune pack makes of the directory that was published. The integrity inside is the one lockfiles already record; a file whose contents no longer match it is refused as corrupt.
Make one locally, with the same validation fune publish runs:
$ fune pack ./registry/finance.tax.add-vat/1.0.0
packed finance.tax.add-vat @1.0.0
integrity sha256-/DEyZA9fylYwsE80wsSfMw+D5OhxEgeolL9agTUscBg=
languages python, rust, typescript
vectors 9
files
capability.json 1,297 B
impl/python.py 925 B
impl/rust.rs 1,527 B
impl/typescript.ts 939 B
vectors.json 2,561 B
sha256 df406777971af1456c0f197c2e213492c34097a86245fa980b422e2575368ccc
5 files, 7,249 B -> finance.tax.add-vat-1.0.0.fune, 8,328 B
--out names the file, or a directory to put it in.
Every published version downloads as one, yanked versions included, from the Download button on its page or directly:
curl -OJ http://www.functionalweave.com/finance.tax.add-vat/1.0.0.fune
shasum -a 256 finance.tax.add-vat-1.0.0.fune
The sha256 to compare against is on the capability's page and in each version's package entry in /index.json. /v1/capabilities/<id>/<version>/download serves the same bytes for scripts.
fune build fetches one package per version and checks its integrity against the index and the lockfile before it generates any code. To install from a file instead of the registry — an air-gapped build, say:
fune add ./finance.tax.add-vat-1.0.0.fune
That pins the file's exact version in fune.project with where it came from, copying the file into vendor/fune/ first if it lives outside the project, so commit it alongside the lockfile:
require finance.tax.add-vat 1.0.0 from=vendor/fune/finance.tax.add-vat-1.0.0.fune
Its dependencies still come from the registry. fune add finance.tax.add-vat switches it back.
One language at a time, like an image tag
The full package describes the whole function and is what you publish. A project only ever runs one language, though, so every version also downloads as one package per language: the same manifest, vectors, README and data, and only that language's implementation. It is named like a Docker tag, <id>-<version>-<language>.cap, and says which language it carries in a "language" field beside format. You are reading this in Python, so:
curl -OJ http://www.functionalweave.com/finance.tax.add-vat/1.0.0/python.fune
fune pull finance.tax.add-vat:python # the same file, checked against the index
fune pull finance.tax.add-vat@1.0.0 # no language: the full package
A reference is <id>[@<version-or-range>][:<language>]. In a project the language defaults to the language line of fune.project, and fune build downloads only that language's package of each version, falling back to the full one from a registry that predates them. Asking a Python project for another language (fune add finance.tax.add-vat:rust) is refused: a project installs one language, and fune pull is how you fetch another.
Each language's package has its own sha256 and its own integrity, listed under package.languages in /index.json. That integrity covers the manifest and exactly the files the package carries, and it is what a project of that language records in its lockfile, whether the version came from the registry, a registry directory, the full .fune or the language's own. fune pack --language python makes the same bytes locally; publishing one is refused, because it is not the whole function.
What a published version is, and how to withdraw one
It is immutable. The files, the metadata and the vectors of 1.1.0 are what they were the moment you uploaded them, for as long as the registry exists. Every version carries an integrity hash over the manifest and every shipped file, and that hash goes into the lockfile of every project that installs it — so an edit in place would break a build somewhere else, and is simply not offered.
That includes rule data. When a VAT rate changes, publish a new version; never re-issue an old one, because somebody's build is pinned to the old answer and may be reprinting a three-year-old invoice.
What you can do is withdraw a version:
fune yank finance.tax.add-vat 1.1.0 --reason "rounds the wrong way on half-pence"
A yanked version stays downloadable, so a lockfile already pinned to it can still fetch its package. It leaves the index, so nothing new resolves it: no fresh install, no range match, no dependency of anything else. The website marks it, and the reason you give is shown there.
Yanking is a toggle — run the same command again to put a version back. It is not a delete: publish a fixed version, and tell people why with the reason.
Then the last step is somebody else's:
fune add finance.tax.add-vat
Which resolves your capability out of http://www.functionalweave.com/index.json, writes the integrity hash into their lockfile, and generates the source into their project.
Publishing a suite
A suite is a named set of functions for one kind of app: sections in build order, each listing core and optional functions with a one-line reason, and the gaps an app like it still needs. Publish one for the apps you build, and any project — or an AI agent planning an app — can start from it.
fune suite new acme.billing-app
fune suite validate acme.billing-app.suite
fune suite publish acme.billing-app.suite --registry http://www.functionalweave.com
fune suite add acme.billing-app # in a project: require every function it lists
The file is the .suite format the registry's own suites use: suite, title, summary, optional synonyms, apps, include and caveat lines, then section <key> <title> blocks of purpose, core <id> <why>, optional <id> <why> and gap <key> <text> lines. The registry accepts it when:
- its id starts with a namespace your organisation owns, and you are a publisher in it (the same rule as functions);
- every function it lists has been uploaded and not rejected — published, or still being verified (a project gets that one once it passes);
- every suite it includes exists, with no cycle, and each line fits on one line (a title of 80 characters, a summary of 240);
- it is at most 64 KB, 60 sections and 300 functions.
A suite has no code, so it skips the sandbox and is live as soon as it is accepted, on /suites, in search and to AI tools. Publishing the same id again replaces it; fune suite unpublish <id> takes it down, unless another suite includes it. The API is POST /v1/suites with {"source": "<the file>"} and DELETE /v1/suites/<id>, with a publishing token.
Hooks: customising a capability in your app
Sometimes a capability is right except for one thing: your invoices round to whole pounds, or your VAT rate comes from your own table. You do not fork it and you do not edit the built file — fune check refuses an edited copy. You write the difference as a function of your own, in your own source, and put a marker comment directly above it. fune build wires that function into the capability’s built code. Nothing on the registry changes, and nobody else’s build is affected.
A marker is one comment line, // in TypeScript and Rust, # in Python:
fune: before <capability-id>[.<fn>]
fune: after <capability-id>[.<fn>]
fune: replace <dependency-id>[.<fn>] in <capability-id|*>
fune: step <capability-id>[.<fn>] after <n|label>
A group’s functions are named <id>.<fn>; a single function needs only the id. There are four kinds:
| Kind | Your function |
|---|---|
| before | Gets the capability’s arguments and returns them, changed or not — or throws to refuse the call. |
| after | Gets the result, and the arguments, and returns the final result. |
| replace | Stands in for a dependency, with the same signature. Inside the one capability named after in, calls to that dependency go to your function; other capabilities that use the dependency are unaffected, unless you write in *. |
| step | Runs at a numbered point inside the entry function’s body. It receives the in-scope values it names as parameters, and may return replacements for them. fune show <id> --steps lists the points, by number and by label. |
One in each language:
// fune: after finance.tax.add-vat
export function roundToPounds(result: VatBreakdown): VatBreakdown { … }
# fune: replace finance.tax.vat-rate in finance.tax.add-vat
def flat_rate(jurisdiction, category, on_date): …
// fune: before money.format
pub fn normalise(amount: &Money) -> &Money { … }
Every capability’s page lists the seams it offers — its functions for before and after, each dependency it requires for replace — as marker lines ready to copy.
Keeping track of it
- The built file says it has been customised. Its header carries a CUSTOMISED: block naming each hook and the function of yours it calls, with its file and line, so nobody reading it mistakes it for the registry’s code:
// ─── fune · finance.tax.add-vat@1.0.0 ─────────────────────────────────── // Add VAT to a net amount using the rate in force on the date of supply. // CUSTOMISED: this build runs code from your project inside finance.tax.add-vat: // after addVat: roundToPounds() from src/billing.ts:14 // replaces finance.tax.vat-rate with charityRate() from src/tax.ts:3 // The shared vectors test the registry's code; `fune verify --hooked` runs them through yours. - fune.lock records the hooks against each capability, so a change to them shows up in review like any other change to what you build.
fune hookslists every hook in the project: the kind, the capability, and your function.fune checkvalidates the markers: each must sit directly above a function, name a capability the project installs, and name a seam that capability actually offers.fune verifystill runs the shared vectors against the unmodified function, so it proves the capability is what the registry says it is.fune verify --hookedalso runs them through your hooked build and reports which answers change. That report is information, not a failure: changing answers is what a hook is for.
Frameworks and language subsets
A capability is written in TypeScript, Python and Rust unless its manifest says otherwise. Some are not: a React component is TypeScript and nothing else, and a UI state helper has no business in a Rust service. Two directives say so, and the registry flags both on every listing and page, as React ^19 · TypeScript only or TypeScript only.
# form.state
id form.state
...
languages typescript
- languages typescript lists the languages it is written in. Left out, it is all three.
fune validateholds the package to it exactly: an implementation for each language listed, and none for any other. - framework react ^19 says it is written for a framework, and which versions of it. A framework implies its languages (React: TypeScript), so languages is left out. The implementation may import react and react-dom and no other third-party code, may be JSX (impl/typescript.tsx), and its signature may use three more types: element (JSX.Element), node (ReactNode) and handler<T> ((value: T) => void). A field written hint? may be left out by the caller, as React props are.
# react.form.text-field
id react.form.text-field
version 1.0.0
level 1
summary A labelled text input with a hint, an error message, prefix and suffix.
framework react ^19
fn TextField(props: TextFieldProps) -> element
type TextFieldProps One question answered in a line of text.
label node the question
id? string the input's id; useId() when left out
hint? node help under the label
onChange? handler<string> called with the new value
require react.form.form-field ^1.0.0
Components and render tests
A component is a function that returns element and takes exactly one parameter, its props, a record type the manifest declares. Its vectors are render vectors: args is the props, and expect is the exact HTML react-dom/server’s renderToStaticMarkup makes of them. A React warning during the render fails the vector, and ids are passed in so the HTML is deterministic. Every function still needs at least eight.
{ "name": "an error marks the input invalid",
"args": [{ "id": "email", "label": "Email address", "error": "Enter an email address" }],
"expect": "<div class=\"fune-field fune-field--error\"><label class=\"fune-label\" for=\"email\">Email address</label>…</div>" }
What a component does when someone types cannot be rendered on a server, so it is tested the way it is written: the state change is a pure function of its own (a reducer, a count, a validator) with ordinary vectors, and the component calls it. A component that holds state or runs effects, any hook but useId, is a client component: its file starts with "use client";, fune validate refuses one that does not, and fune build keeps the directive as the first statement of the built file, which Next.js needs. A component with no state renders on the server and ships no JavaScript. Its page says which it is.
What may require what
A capability may only require one written in every language it is. A React component may require auth.password-policy, which runs in all three; auth.password-policy could never require a component, and a Python-and-Rust capability cannot require a TypeScript-only one. fune validate names the dependency and what it lacks.
Building and verifying
- A project can only use what is written in its language.
fune buildin a Python or Rust project that names a TypeScript-only or React capability stops, saying so and where it was named. - In a TypeScript project, a React capability needs React itself, which fune does not ship: without react and react-dom installed,
fune buildstops with the line to run,npm install react react-dom. fune verify --allruns a TypeScript-only capability in TypeScript alone, and renders a React one with the React in tooling/react; a project’s ownfune verifyrenders with the project’s.- A .fune has a package for each of its languages only: a React capability’s is <id>-<version>-typescript.fune, and its page offers no Python or Rust download.
Browse them with React only, or the React form elements collection: accessible form components and the pure helpers they use.
Use Functional Weave from AI tools
A coding agent about to write a function should look here first: if a capability fits, the project gets one line and tested code instead of new code to review. Three things make that the agent’s habit.
The MCP server
This registry is an MCP server at http://www.functionalweave.com/mcp: Streamable HTTP, stateless, read-only, and open without an account. Its tools are plan_app (call it first when someone describes an app to build: the suite for that kind of app, in build order, and the gaps), search_functions (call it before writing a function), get_function (signatures, worked examples, status, license, the one-line reference and the hooks), get_source, list_categories, get_suite and list_suites, with a find-or-write prompt. fune mcp is the same server on stdio, run inside a project: it reads the project’s own registry (a directory works offline) and adds project_info, add_function, add_suite and check_project.
Claude Code:
# the registry, remote (read-only, no account needed)
claude mcp add --transport http fune http://www.functionalweave.com/mcp
# or the local server, inside a project: its registry, its language, and
# project_info, add_function and check_project as well
claude mcp add fune-local -- fune mcp
Cursor and VS Code, in the project:
// .cursor/mcp.json
{
"mcpServers": {
"fune": { "url": "http://www.functionalweave.com/mcp" },
"fune-local": { "command": "fune", "args": ["mcp", "--project", "${workspaceFolder}"] }
}
}
// .vscode/mcp.json
{
"servers": {
"fune": { "type": "http", "url": "http://www.functionalweave.com/mcp" },
"fune-local": { "type": "stdio", "command": "fune", "args": ["mcp", "--project", "${workspaceFolder}"] }
}
}
The agent skill
The repository’s ai/skills/fune/ is an Agent Skill (Claude Code and other tools that read SKILL.md). It triggers whenever the agent is about to implement a function, calculation, validation, formatting or business rule, and walks it through search, read, use or write. Copy the folder into .claude/skills/ in a project, or ~/.claude/skills/ for every project. Without a skill, paste this into the project’s AGENTS.md or CLAUDE.md:
## Functions: search Functional Weave first
When the user describes an app to build, call the Functional Weave MCP server's plan_app
first (or `fune search "<description>"` and `fune suite <id>`) and show
them what Functional Weave covers and the gaps you will write.
Before writing a utility, calculation, validation, formatting rule or piece of
business logic, search Functional Weave (its MCP server's search_functions, or
`fune search <words>`). If a capability fits, use its one-line reference in
the file that calls it and run `fune build`; never copy its code or edit
.fune/build. Customise one with a hook, not a fork. Tell the user before
relying on one whose status is unreviewed. If nothing fits, write it yourself.
llms.txt
/llms.txt is the registry in brief, for a model reading the site; /llms-full.txt is every published capability on one line, with its signatures and status.