Writing MCP Tools with Required Annotations
This document is for developers who add or maintain @rpc methods that are exposed as MCP tools. To be listed in the Claude MCP Directory, every tool must declare a safety annotation. This plugin supports that via docblock tags.
Why annotations are required
Anthropic’s MCP Directory requires every tool to have exactly one of:
readOnlyHint: true– the tool only reads data; it does not modify state or send external requests.destructiveHint: true– the tool may modify data, write files, send requests, or have other side effects.
Without these, your server cannot be approved for the catalog. The MCPServer plugin maps docblock annotations to these MCP fields automatically.
How to annotate your RPC methods
The plugin supports exactly two safety annotations. Add one of them to the docblock of each @rpc method. Both are registered with the RPC annotation parser so they appear in parsed method info.
| Docblock tag | PHP constant (McpAnnotationResolver) |
readOnlyHint | destructiveHint |
|---|---|---|---|
@mcpReadOnly |
ANNOTATION_MCP_READ_ONLY ('mcpReadOnly') |
true |
false |
@mcpDestructive |
ANNOTATION_MCP_DESTRUCTIVE ('mcpDestructive') |
false |
true |
When to use @mcpReadOnly
- Only reads from DB, files, or APIs.
- No creates, updates, deletes, or writes.
- No side effects (no emails, webhooks, or external calls).
- Internal caching only is still considered read-only.
When to use @mcpDestructive
- Creates, updates, or deletes data or resources.
- Writes files (including temporary files).
- Sends emails, notifications, or webhooks.
- Calls external APIs that change state.
- Any other side effect.
Examples
Read-only tool
/**
* @rpc getProjectByID
* @param int $idProject
* @return array
* @mcpReadOnly
*/
public function getProjectByID(int $idProject): array
{
// Only reads from DB; no modifications.
return $this->projectFacade->getByID($idProject);
}
Destructive tool
/**
* @rpc createTask
* @param string $title
* @param int $idProject
* @return array
* @mcpDestructive
*/
public function createTask(string $title, int $idProject): array
{
// Creates a new task; modifies data.
return $this->_taskStore->create(['title' => $title, 'id_project' => $idProject]);
}
Another destructive tool (external request)
/**
* @rpc sendNotification
* @param string $userId
* @param string $message
* @return bool
* @mcpDestructive
*/
public function sendNotification(string $userId, string $message): bool
{
// Sends an external request (e.g. email or webhook).
return $this->_notificationService->send($userId, $message);
}
Default when no annotation is present
If you do not add @mcpReadOnly or @mcpDestructive, the plugin defaults to destructiveHint: true (and readOnlyHint: false). That keeps existing tools valid but is conservative: catalog reviewers expect every tool to be explicitly categorized, so you should add the correct annotation for each method.
Optional: human-readable title
The MCP protocol allows an optional title for tools (a short, human-readable name for UIs).
This plugin does read a title from docblocks via the @mcpTitle annotation.
@mcpTitle Some short label– sets the MCP tool'stitlefield.- If no
@mcpTitleis present, the tool has no explicittitle(clients can still showname/description).
Example:
/**
* @rpc getProjectById
* @param int $idProject
* @return array
* @mcpReadOnly
* @mcpTitle Get project by ID
*/
public function getProjectById(int $idProject): array
{
...
}
Summary
| Docblock tag | readOnlyHint | destructiveHint | Use for |
|---|---|---|---|
@mcpReadOnly |
true |
false |
Read-only tools |
@mcpDestructive |
false |
true |
Tools that modify or have side effects |
| (none) | false |
true |
Default; prefer adding an explicit tag |
After adding or changing annotations, run sync RPC methods so the server’s tools/list is up to date. For catalog submission, ensure every tool has the correct annotation and see ./McpClaudeServerRegistrationChecklist.md.