Skip to content

AI agents ​

An agent can answer "is this contract signed, and by whom" by reading the document itself, and, with a person's approval, sign one. The package provides the tools for both, on top of Laravel's own AI packages. It does not provide the agent: instructions, provider, model and cost are yours to choose.

NeedsReached byWhat it does
validate_pdf_signaturelaravel/mcpMCP clients and AI SDK agentsverifies every signature, says who signed
list_signature_fieldslaravel/mcpMCP clients and AI SDK agentslists the fields, signed and empty
sign_pdflaravel/aiAI SDK agentssigns, after a person approves

Reading goes through MCP and signing through the AI SDK, and the split is deliberate. The AI SDK runs MCP tools, so a read tool written once reaches both worlds. Signing needs a person to approve every call, and only the AI SDK lets a tool insist on that: on the MCP side, approval is whatever the client was set to do (0040).

Installing ​

Both SDKs are optional. Install the ones you use:

bash
composer require laravel/mcp   # the two read tools, and the MCP server
composer require laravel/ai    # the signing tool

The package requires ^1.0 of each and refuses to install beside anything else, so a version mismatch fails at composer require rather than on the first tool call. Nothing is registered for you: no route, no tool, no binding.

Opening a disk ​

Out of the box, no tool reaches anything. Agents address documents by a Storage disk and a path, and a disk is reachable only once you list it:

php
// config/a1-pdf-sign.php
'agents' => [
    'disks' => ['contracts'],
],

Open a disk that holds what agents should see and nothing else. A disk scoped to a directory is the simplest way to get there:

php
// config/filesystems.php
'contracts' => [
    'driver' => 's3',
    'bucket' => env('AWS_BUCKET'),
    'root' => 'contracts',
    // …
],

The disk list decides what is reachable at all. Who may reach which document is your gate.

What is refused ​

Every path a model sends goes through Agents\DocumentAccess before a byte is read, and a tool's schema offers the open disks as an enum. The enum is a hint to the model; the guard is the control:

RefusedBecause
a disk not in agents.disksnobody opened it
/etc/deal.pdf, C:/deal.pdfabsolute: a path is relative to its disk
2026/../../salaries.pdf.. anywhere is refused, not normalised
contracts\deal.pdf, a null bytenot a path a disk understands
notes.txt, deal.pdf.phponly names ending in .pdf
a document over agents.max_bytesthe model chooses the file, and the engine holds it in memory
a destination that existsa signed copy never overwrites

The refusal comes back to the model as the tool's error, worded so it can correct itself: it names the disks that are open, never a disk's root.

A path is relative to its disk, and never starts with the disk's name.deal.pdf on the disk contracts is deal.pdf, not contracts/deal.pdf. The tools tell the model so in their schema, because a model left to guess glues the disk's name to the front, as DeepSeek did the first time this was tried against a real provider.

The size limit is 50 MB by default, read from the disk's own metadata before a byte is loaded. Null removes it:

php
'agents' => [
    'max_bytes' => 50 * 1024 * 1024,
],

Who may reach which document ​

A disk holds every customer's contracts; the user talking to the agent may see their own. That is a question Laravel already answers with a gate, so the tools ask yours (0041):

php
use LSNepomuceno\LaravelA1PdfSign\Agents\Ability;

// AppServiceProvider::boot()
Gate::define(Ability::Read->value, function (User $user, string $disk, string $path) {
    return Contract::where('path', $path)->where('customer_id', $user->customer_id)->exists();
});

Gate::define(Ability::Sign->value, function (User $user, string $disk, string $path, string $destinationDisk, string $destinationPath) {
    return $user->can('sign', Contract::firstWhere('path', $path));
});
AbilityAsked byReceives
a1-pdf-sign.agents.readvalidate_pdf_signature, list_signature_fieldsthe user, the disk, the path
a1-pdf-sign.agents.signsign_pdfthe user, the disk, the path, and where the copy would go

What to know about it:

  • An ability you have not defined allows. Until you define one, the disk list is the only control, exactly as in 3.1.0.
  • Once defined, a guest is refused unless the ability's user is nullable, as Laravel does everywhere. An MCP server over stdio has no user, so a Mcp::local server with a defined ability needs a nullable user to answer anything.
  • The gate is asked before the document is looked at. A refused user reads "you may not read", never "there is no document", so a refusal does not tell them what exists.
  • The user is whoever authenticated the request: the MCP route's guard, or the default guard when an AI SDK agent runs. An agent on a queue has no user; see signing on a queue.

Agents\DocumentAccess::authorize() and allows() are public, so a tool of your own can ask the same question the same way.

The CPF stays out of the model ​

The validation tool reports each signer's name. The CPF or CNPJ on an ICP-Brasil certificate is personal data, and sending it to a model provider is your decision to make:

php
'agents' => [
    'expose_registry' => env('A1_PDF_SIGN_AGENTS_EXPOSE_REGISTRY', false),
],

Where to go next ​

  • Reading through MCP: serving the tools to Claude Code, Cursor or Boost, and handing them to an AI SDK agent.
  • Signing through an agent: the approval flow, choosing the certificate, and what happens after.
  • Testing: faking the model and asserting on the tools.

Version 3.2.0. Released under the MIT License.