Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Site Agent is free and open source. The complete release ZIP and a free automati

## Companion Package

Release 0.4.0 also includes `site-agent-companion-0.4.0.zip`, built from `companion/site-agent/`. It supplies the approved icon, a credential-free MCP connection and a WordPress workflow skill for compatible ChatGPT/Codex hosts. The skill covers site inspection, raw-content audits, draft preparation, hash-checked edits and enabled developer tools.
Release 0.4.1 also includes `site-agent-companion-0.4.1.zip`, built from `companion/site-agent/`. It supplies the approved icon, a credential-free MCP connection and a WordPress workflow skill for compatible ChatGPT/Codex hosts. The skill covers site inspection, raw-content audits, draft preparation, hash-checked edits and enabled developer tools.

This companion archive is separate from the WordPress installable ZIP and contains no credentials. With OAuth connections on (0.4.0+), an OAuth-capable host signs in through WordPress and needs nothing else. Otherwise, authentication is configured privately through the host with a WordPress Application Password in a Basic Authorization header or an explicitly enabled authenticated URL. ChatGPT web compatibility with credential-bearing URLs has not been verified.

Expand Down Expand Up @@ -128,7 +128,7 @@ Background WP-CLI jobs, an AI chat UI and a recoverable PHP sandbox are outside

## OAuth connections

Version 0.4.0 adds opt-in OAuth 2.1 following the MCP authorization specification. Turn on **OAuth connections** in Tools → Site Agent, then add the endpoint to an OAuth-capable client with no credentials:
Version 0.4.0 added OAuth 2.1 following the MCP authorization specification, and from 0.4.1 **OAuth connections** are on by default whenever Site Agent is enabled (a site that turned them off keeps that choice). It is the recommended way to connect: add the endpoint to an OAuth-capable client with no credentials. Application Passwords keep working, but Tools → Site Agent warns about them, and `site-context` returns a `connection` warning that agents pass on whenever a call uses one.

- An unauthenticated MCP request answers 401 with `WWW-Authenticate: Bearer resource_metadata="…/wp-json/site-agent/v1/oauth/protected-resource"`.
- Protected resource metadata (RFC 9728) and authorization server metadata (RFC 8414) are served through REST and at the path-inserted well-known URLs, `/.well-known/oauth-protected-resource/wp-json/site-agent/v1/mcp` and `/.well-known/oauth-authorization-server/wp-json/site-agent/v1/oauth`, plus the OpenID Connect forms, including `/wp-json/site-agent/v1/oauth/.well-known/openid-configuration`. The issuer is `…/wp-json/site-agent/v1/oauth`, so Site Agent never takes over the site-root discovery documents that another plugin's OAuth server may serve (Rank Math's MCP server does).
Expand Down Expand Up @@ -167,7 +167,7 @@ python3 bin/sync-skills.py
bash bin/build.sh
```

Tests refuse to load an installation without a `.site-agent-test-install` marker. Never place that marker on a real site. Integration tests modify disposable options, users, posts, and fixture files. The build uses a runtime allowlist and excludes tests, development dependencies, Composer metadata, docs, and screenshots. Ship `dist/site-agent-0.4.0.zip`, not a GitHub source archive.
Tests refuse to load an installation without a `.site-agent-test-install` marker. Never place that marker on a real site. Integration tests modify disposable options, users, posts, and fixture files. The build uses a runtime allowlist and excludes tests, development dependencies, Composer metadata, docs, and screenshots. Ship `dist/site-agent-0.4.1.zip`, not a GitHub source archive.

## License and Contributions

Expand Down
2 changes: 1 addition & 1 deletion companion/site-agent/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"termsOfServiceURL": "https://gauravtiwari.org/tos/"
},
"name": "site-agent",
"version": "0.4.0",
"version": "0.4.1",
"description": "A WordPress assistant for site inspection, content audits, draft creation, and hash-checked edits through Site Agent's MCP tools.",
"author": {
"name": "Gaurav Tiwari",
Expand Down
2 changes: 1 addition & 1 deletion companion/site-agent/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "site-agent",
"version": "0.4.0",
"version": "0.4.1",
"description": "A WordPress assistant for site inspection, content audits, draft creation, and hash-checked edits through Site Agent's MCP tools.",
"author": {
"name": "Gaurav Tiwari",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

Endpoint supplied by the owner: https://gauravtiwari.org/wp-json/site-agent/v1/mcp
Transport: Streamable HTTP.
Server source requires HTTPS. It accepts OAuth 2.1 bearer tokens when the site turns on OAuth connections (Site Agent 0.4.0+), and native WordPress Application Passwords through a Basic Authorization header or explicitly enabled URL authentication. The supplied Authorization value was a placeholder, not a usable credential.
Server source requires HTTPS. It accepts OAuth 2.1 bearer tokens (on by default from Site Agent 0.4.1; recommended), and native WordPress Application Passwords through a Basic Authorization header or explicitly enabled URL authentication. The supplied Authorization value was a placeholder, not a usable credential.

This package deliberately contains no Authorization header or credentials. Use a host-supported private connection setting to supply authentication if the host supports this server's Basic header requirement. If the host requires OAuth, the site owner turns on OAuth connections in Tools > Site Agent; the host then discovers the sign-in page from the endpoint's 401 response and the administrator approves it on a WordPress consent screen. If OAuth is off and the host cannot configure headers or retain an authenticated URL, the direct connection is blocked; report that limitation. Do not claim installation authenticates the server, invent an OAuth endpoint, publish credentials in a manifest, or silently deploy a proxy.

WordPress setup: install the complete Site Agent release ZIP, open Tools > Site Agent, enable access and required groups, and create a dedicated Application Password in the administrator's profile. On multisite, server access requires super-administrator rights. Keep developer groups off unless the task needs them.

Never ask for credentials in chat. The Tools > Site Agent converter runs in the browser and can generate a Basic header for a compatible client. Use secure host credential entry when available. Disabling Site Agent or revoking the dedicated Application Password revokes access.

Verification after connection: discover live tools, call the harmless site-context tool, verify site_url and enabled_tools, then read existing content. Do not test authentication with a write.
Verification after connection: discover live tools, call the harmless site-context tool, verify site_url and enabled_tools, then read existing content. If site-context returns connection.warning (an Application Password or authenticated URL is in use), tell the user and recommend reconnecting with OAuth. Do not test authentication with a write.

The server also supports explicitly enabled URL authentication via an auth query parameter containing Base64-encoded username and Application Password credentials. Keep the complete URL only in a host-supported private connection setting, never in manifests, instructions or reports. Each request must retain its query parameters. OAuth-only hosts should use OAuth connections instead; clients that discard the query and lack OAuth are not supported. Do not claim universal client support.
10 changes: 9 additions & 1 deletion includes/class-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public static function definitions(): array {
return array(
'site-context' => array(
'label' => __( 'Site context', 'site-agent' ),
'description' => __( 'Inspect WordPress, PHP, active plugins, theme, page builders, post types, taxonomies and the tools this connection can use. Start here.', 'site-agent' ),
'description' => __( 'Inspect WordPress, PHP, active plugins, theme, page builders, post types, taxonomies, the tools this connection can use and how it authenticated. Start here. If connection has a warning, pass it on to the user.', 'site-agent' ),
'group' => '',
'callback' => array( Content::class, 'context' ),
'input' => array(),
Expand All @@ -163,6 +163,14 @@ public static function definitions(): array {
'taxonomies' => self::list_of( array( 'type' => 'string' ) ),
'enabled_tools' => self::list_of( array( 'type' => 'string' ) ),
'builders' => self::list_of( array( 'type' => 'object' ) ),
'connection' => self::object(
array(
'method' => array( 'type' => 'string' ),
'client' => array( 'type' => 'string' ),
'warning' => array( 'type' => 'string' ),
),
array( 'method' )
),
),
array( 'site_url', 'wordpress', 'enabled_tools' )
),
Expand Down
87 changes: 82 additions & 5 deletions includes/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ private static function controls(): array {
);
return array(
'enabled' => array( __( 'Enable Site Agent', 'site-agent' ), __( 'Allow authenticated administrators to connect and use the tools selected below.', 'site-agent' ) ),
'url_auth' => array( __( 'URL authentication', 'site-agent' ), __( 'Allow Base64-encoded username and Application Password credentials in the MCP endpoint auth query parameter. URLs may be recorded in client history and server logs. Use a dedicated, revocable Application Password.', 'site-agent' ) ),
'oauth' => array( __( 'OAuth connections', 'site-agent' ), __( 'Let MCP clients that support OAuth, such as Claude and ChatGPT connectors, connect by signing in here. An administrator approves each client on a consent screen and picks its tool groups. Clients get one-hour access tokens and refresh tokens that end after 30 days without use. No password is shared with the client.', 'site-agent' ) ),
'url_auth' => array( __( 'URL authentication', 'site-agent' ), __( 'Not recommended. Allow Base64-encoded username and Application Password credentials in the MCP endpoint auth query parameter. The password is only encoded, and URLs can be recorded in client history, chats and server logs. Use OAuth connections instead whenever the client supports them.', 'site-agent' ) ),
'oauth' => array( __( 'OAuth connections (recommended)', 'site-agent' ), __( 'On by default. Let MCP clients that support OAuth, such as Claude and ChatGPT connectors, connect by signing in here. An administrator approves each client on a consent screen and picks its tool groups. Clients get one-hour access tokens and refresh tokens that end after 30 days without use. No password is shared with the client.', 'site-agent' ) ),
'content_write' => array( __( 'Content writes', 'site-agent' ), __( 'Create drafts, edit posts or pages, set terms, featured images and SEO fields, and import media. Edits to live posts are staged as autosaves unless a status is passed explicitly.', 'site-agent' ) ),
'file_read' => array( __( 'Source inspection', 'site-agent' ), __( 'Read plugin, theme and must-use plugin source files. Source files can contain sensitive data.', 'site-agent' ) ),
'file_write' => array( __( 'Source editing', 'site-agent' ), __( 'Create, overwrite, move or delete plugin and theme files, including PHP. PHP changes that cause a fatal error are reverted when the site can be checked.', 'site-agent' ) ),
Expand Down Expand Up @@ -150,6 +150,7 @@ public static function render(): void {
<?php if ( Config::execution_blocked() ) : ?>
<div class="notice notice-warning inline"><p><?php esc_html_e( 'SITE_AGENT_ALLOW_EXECUTION is false in wp-config.php. Source editing, PHP, and WP-CLI tools are blocked on this site.', 'site-agent' ); ?></p></div>
<?php endif; ?>
<?php self::connection_warnings( $config ); ?>
<form method="post" action="options.php">
<?php settings_fields( 'site_agent' ); ?>
<table class="form-table" role="presentation">
Expand All @@ -169,15 +170,22 @@ public static function render(): void {
<?php submit_button(); ?>
</form>
<?php self::oauth_panel(); ?>
<?php self::partners_panel(); ?>
<?php self::diagnostics_panel(); ?>
<?php self::license_panel(); ?>
<h2><?php esc_html_e( 'Connect a client', 'site-agent' ); ?></h2>
<ol>
<li><?php esc_html_e( 'Enable Site Agent and save the selected tools.', 'site-agent' ); ?></li>
<li><a href="<?php echo esc_url( admin_url( 'profile.php#application-passwords-section' ) ); ?>"><?php esc_html_e( 'Create a dedicated WordPress Application Password in your profile.', 'site-agent' ); ?></a></li>
<li><?php esc_html_e( 'Use the endpoint below in a client that supports Streamable HTTP and a custom Authorization header. The header uses HTTP Basic authentication with your username and Application Password. Keep the client configuration private.', 'site-agent' ); ?></li>
<li><?php esc_html_e( 'Enable Site Agent and the tools you need, and keep OAuth connections on. Save.', 'site-agent' ); ?></li>
<li><?php esc_html_e( 'Add the endpoint below to your MCP client, such as a Claude or ChatGPT connector, Claude Code or Cursor, with no credentials.', 'site-agent' ); ?></li>
<li><?php esc_html_e( 'The client opens a WordPress sign-in and consent page. Check the client name, choose the tool groups it may use, and allow it.', 'site-agent' ); ?></li>
</ol>
<p><strong><?php esc_html_e( 'Endpoint', 'site-agent' ); ?>:</strong> <code><?php echo esc_html( $endpoint ); ?></code></p>
<h2><?php esc_html_e( 'Application Passwords (not recommended)', 'site-agent' ); ?></h2>
<div class="notice notice-warning inline"><p><?php esc_html_e( 'Use an Application Password only for a client that cannot sign in with OAuth. It acts as your account until you revoke it, every client configuration that holds it can leak it, and a password in a URL can also end up in history, chats and server logs. OAuth tokens expire, can be limited per client, and never reveal a password.', 'site-agent' ); ?></p></div>
<ol>
<li><a href="<?php echo esc_url( admin_url( 'profile.php#application-passwords-section' ) ); ?>"><?php esc_html_e( 'Create a dedicated WordPress Application Password in your profile, one per client.', 'site-agent' ); ?></a></li>
<li><?php esc_html_e( 'Use the endpoint above in a client that supports Streamable HTTP and a custom Authorization header. The header uses HTTP Basic authentication with your username and Application Password. Keep the client configuration private, and limit the password\'s tools above.', 'site-agent' ); ?></li>
</ol>
<p><?php esc_html_e( 'Remote connections require HTTPS. Plain HTTP is accepted only when WordPress identifies the installation as local. Clients that require OAuth can connect with the endpoint alone once OAuth connections are on: they find the sign-in page through the endpoint\'s OAuth metadata.', 'site-agent' ); ?></p>
<section id="site-agent-url-auth-guide" aria-labelledby="site-agent-url-auth-heading" style="margin-block:24px;padding:24px;background:#fff;border:1px solid #c3c4c7;">
<h3 id="site-agent-url-auth-heading"><?php esc_html_e( 'Connect with an authenticated URL', 'site-agent' ); ?></h3>
Expand All @@ -203,6 +211,75 @@ public static function render(): void {
<?php
}

/**
* Warnings when connections bypass OAuth: OAuth off, URL authentication on, or recent calls
* made with an Application Password.
*
* @param array<string, mixed> $config Settings.
*/
private static function connection_warnings( array $config ): void {
if ( empty( $config['enabled'] ) ) {
return;
}
$messages = array();
if ( empty( $config['oauth'] ) ) {
$messages[] = __( 'OAuth connections are off, so clients can only connect with an Application Password. Turn OAuth connections on below; it is the recommended way to connect.', 'site-agent' );
}
if ( ! empty( $config['url_auth'] ) ) {
$messages[] = __( 'URL authentication is on. A password in a URL is only encoded and can be recorded in client history, chats and server logs. Reconnect those clients with OAuth, then turn URL authentication off and revoke their Application Passwords.', 'site-agent' );
}
$since = time() - 30 * DAY_IN_SECONDS;
$labels = array();
foreach ( (array) get_option( Audit::OPTION, array() ) as $row ) {
if ( is_array( $row ) && in_array( $row['via'] ?? '', array( 'header', 'url' ), true ) && strtotime( (string) ( $row['time'] ?? '' ) ) >= $since ) {
$labels[ (string) ( $row['credential'] ?? '' ) ] = true;
}
}
if ( $labels ) {
$names = array_filter( array_keys( $labels ) );
$messages[] = sprintf(
/* translators: %s: Application Password names. */
__( 'Tool calls in the last 30 days used Application Passwords (%s). Reconnect those clients with OAuth if they support it, then revoke the passwords in your profile.', 'site-agent' ),
$names ? implode( ', ', $names ) : __( 'unnamed', 'site-agent' )
);
}
foreach ( $messages as $message ) {
echo '<div class="notice notice-warning inline"><p>' . esc_html( $message ) . '</p></div>';
}
}

/** Plugins that add skills for connected agents, with links and their state on this site. */
private static function partners_panel(): void {
?>
<section id="site-agent-plugin-skills" aria-labelledby="site-agent-plugin-skills-heading" style="margin-block:24px;padding:24px;background:#fff;border:1px solid #c3c4c7;">
<h2 id="site-agent-plugin-skills-heading"><?php esc_html_e( 'Plugin skills', 'site-agent' ); ?></h2>
<p><?php esc_html_e( 'These plugins hand connected agents a skill for their own workflows. Agents find active ones with list-skills and get-skill, with nothing else to install.', 'site-agent' ); ?></p>
<table class="widefat striped" style="max-width:900px"><thead><tr><th><?php esc_html_e( 'Plugin', 'site-agent' ); ?></th><th><?php esc_html_e( 'What agents can do', 'site-agent' ); ?></th><th><?php esc_html_e( 'On this site', 'site-agent' ); ?></th></tr></thead><tbody>
<?php foreach ( Skills::partners() as $partner ) : ?>
<tr>
<td><a href="<?php echo esc_url( $partner['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $partner['name'] ); ?></a></td>
<td><?php echo esc_html( $partner['does'] ); ?></td>
<td>
<?php
if ( 'active' === $partner['state'] && $partner['skills'] ) {
/* translators: %s: skill names. */
echo esc_html( sprintf( __( 'Active. Skills: %s', 'site-agent' ), implode( ', ', $partner['skills'] ) ) );
} elseif ( 'active' === $partner['state'] ) {
esc_html_e( 'Active, without a skill yet. Update the plugin, or turn on the module that provides it.', 'site-agent' );
} elseif ( 'installed' === $partner['state'] ) {
echo '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">' . esc_html__( 'Installed, not active', 'site-agent' ) . '</a>';
} else {
echo '<a href="' . esc_url( $partner['url'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Get it', 'site-agent' ) . '</a>';
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody></table>
</section>
<?php
}

/** OAuth discovery details and the current user's connected clients, with revoke buttons. */
private static function oauth_panel(): void {
if ( ! Config::get()['oauth'] ) {
Expand Down
3 changes: 2 additions & 1 deletion includes/class-config.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public static function defaults(): array {
return array(
'enabled' => false,
'url_auth' => false,
'oauth' => false,
// OAuth is the recommended way to connect, so it is on whenever Site Agent itself is enabled.
'oauth' => true,
'content_write' => false,
'file_read' => false,
'file_write' => false,
Expand Down
1 change: 1 addition & 0 deletions includes/class-content.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static function context(): array {
'taxonomies' => array_values( get_taxonomies( array( 'show_in_rest' => true ) ) ),
'enabled_tools' => $tools,
'builders' => $builders,
'connection' => Scopes::connection(),
);
}

Expand Down
Loading
Loading