Skip to content

feat: perry/tools — expose TypeScript functions as App Intents (Apple) and MCP tools (all platforms) #3888

@warren-gallagher

Description

@warren-gallagher

The problem
There is no way to expose Perry functions as callable tools for AI agents. Two concrete scenarios where this matters today:

Scenario 1 — Apple Intelligence on iOS/iPadOS/macOS. Apple Intelligence discovers and invokes app capabilities via App Intents. A Perry app has no way to declare intents, so it is invisible to Apple Intelligence and to the Shortcuts app. Writing the required Swift conformances by hand defeats the purpose of writing TypeScript.

Scenario 2 — MCP on any platform. MCP (Model Context Protocol) is the emerging standard for exposing callable tools to AI models. A Perry binary compiled for macOS or Linux cannot act as an MCP server, so tools built in Perry are unavailable to Claude Desktop, local models, or any MCP-aware agent.

The underlying gap is the same in both cases: Perry has no mechanism to declare a function as externally callable with typed parameters and a result, and no way to surface that declaration to the outside world.

Proposed solution
A perry/tools module exposing a @tool method decorator. The same decorated function compiles to native App Intents on Apple platforms, emits a portable JSON manifest everywhere, and serves as an MCP server when the binary is launched with --mcp-server.

import { tool, param } from "perry/tools";

class GitTools {
  @tool({
    title: "Run Git Command",
    description: "Execute any git operation on a local repository",
    category: "create",
    domain: "git",
    phrases: ["run git \\(.command) in \\(.applicationName)"],
    mcp: {
      // Richer LLM-facing description for MCP context
      description: "Execute any git command. Use for log, diff, commit, push, " +
                   "branch, checkout, status. Pass the full git invocation as command."
    }
  })
  async runGit(
    @param({ title: "Command" }) command: string,
    @param({ title: "Repository Path", required: false }) repoPath?: string
  ): Promise<string> {
    return await git(command, repoPath);
  }
}

New CLI flags:

perry compile --emit-intents=intents.json — emit the tool manifest (JSON, Markdown, or MCP schema)
./my-app --mcp-server — start an MCP JSON-RPC server on stdin/stdout
On Apple targets the compiler emits a Swift AppIntent conformance (iOS 16+ / macOS 13+) and automatically adds an AssistantSchemas retroactive conformance extension for Apple Intelligence routing on iOS 18+ / macOS 15+, without the developer specifying version floors.

@appIntent is exported as an alias for tool for Apple developers who prefer the explicit name.

Alternatives considered
Platform-branded annotations (@appIntent, @mcptool). Branding the annotation to a platform forces the developer to choose a deployment target at authoring time and fragments the API surface. The underlying concept — a named function with typed parameters callable by an external agent — is platform-neutral.

Double annotation for dual targets. Allowing @appIntent and @mcptool to stack on the same method. Rejected: two decorators for the same concept is redundant and the metadata overlap requires reconciliation. Platform-specific overrides belong inside the single @tool annotation as optional sub-objects (apple, mcp).

defineIntent({...}) function call (like Widget({...})). The existing widget pattern uses a function call at module level. For method-level metadata a decorator is more idiomatic TypeScript and keeps the declaration co-located with the implementation. The lowering mechanism (compile-time annotation recognition) is identical either way.

Separate MCP server crate / daemon. A standalone daemon aggregating manifests from multiple Perry apps is a valid future extension but imposes unnecessary complexity for the single-app use case. Embedding --mcp-server in the binary keeps the deployment story simple: Claude Desktop points directly at the app binary.

Scope
Small — CLI flag, new stdlib function, doc page
Medium — new widget, new code-gen pass, new perry.toml field
Large — new platform target, new backend, cross-cutting compiler change
A detailed phased plan designed to minimise merge conflicts with Perry's fast release cadence is in docs/src/internals/perry-tools-plan.md. The first four PRs (HIR node, stdlib stub, decorator lowering, manifest crate) are purely additive with no effect on existing apps and no dependency on Apple platform decisions.

Additional context
Prior art:

Apple App Intents framework — developer.apple.com/documentation/appintents — iOS 16+ / macOS 13+
Apple AssistantSchemas for Apple Intelligence routing — iOS 18+ / macOS 15+
MCP specification — modelcontextprotocol.io
Existing Perry infrastructure this builds on:

perry-codegen-swiftui already emits AppIntentTimelineProvider and WidgetConfigurationIntent for WidgetKit — the Swift codegen pattern is established
perry-hir already lowers class/method decorators and emits design:paramtypes — the decorator recognition hook is in place
perry-api-manifest already emits JSON/Markdown manifests for stdlib APIs — the manifest emit pattern is established

The following is a plan that proposes how this could be accomplished and that I would be willing to implement if this is desired by the maintainers.
perry-tools-plan.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions