Files
openseo/manifest.ts
pastilhas 037cbf9288 declare the MCP path, so the agent gets OpenSEO's own tools
A path, resolved against this Officer, so the agent reaches /api/openseo/mcp
through the same gate as everything else and never the instance directly.

Not wrapped as Officer-native tools: the vocabulary moves upstream, a copy would
go stale, and it fails silently — a tool exists that Officer cannot see.
2026-08-29 18:53:12 +01:00

85 lines
3.9 KiB
TypeScript

import type { PluginManifest } from '@@/plugins/manifest';
/**
* OpenSEO — self-hosted keyword research, rank tracking and site audits, inside Officer.
*
* ── It points at an instance; it does not own one ──
*
* OpenSEO is a container, and a plugin has no primitive for "bring a container": `osDependencies`
* installs OS packages. So this follows the shape `jellyfin`, `gitea` and `proxy-manager` already use —
* a stored connection to a service running somewhere, with the credential write-only across the
* boundary.
*
* A decision to revisit rather than a permanent one. `todo/infra/opm-officer-s-own-process-manager` is
* heading toward the Docker network, and on the day a plugin can declare a container, the connection
* becomes something this provisions rather than something somebody types.
*
* ── The permission split, and why it is not the same as proxy-manager's ──
*
* Proxy Manager carves out `/_config` because its credential can redirect any hostname on the machine.
* The shape here is the same and the reason is different: OpenSEO self-hosts with
* `AUTH_MODE=local_noauth`, so ANY reachable request to it is unauthenticated by design — the whole of
* Officer's gate is the authentication. A member who could read the connection could reach the instance
* directly and bypass this plugin entirely.
*
* So `/_config` is owner-only, and the pass-through is the only door. Nothing here may publish
* OpenSEO's own port, and that is not a hardening measure — it is the security model upstream
* documents: *"only expose it behind your own auth-protected reverse proxy, tunnel, or private
* network."*
*
* ── Why the tools are not reimplemented here ──
*
* OpenSEO ships its own MCP server at `/mcp`. Most integrations need Officer to invent the agent-facing
* surface; this one arrives with one, built for exactly this kind of client. Wrapping it in
* Officer-native tools is a real option and an open decision — but inventing a second vocabulary for
* tools that already exist would be work whose only product is drift.
*/
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '0.1.0',
platform: '>=1.0.0',
label: 'OpenSEO',
summary: 'Keyword research, rank tracking, competitors, backlinks and site audits — self-hosted',
/*
`Marketing` and `Research` would both be more accurate and neither exists — the union is closed, and
extending it is a platform change rather than a plugin's to make. `Development` is the least wrong of
what is there: this is a tool about websites, used by whoever builds them. Worth revisiting if the
store ever grows a marketing shelf.
*/
tags: ['Development', 'Homelab'],
icon: 'TrendingUp',
color: '#22c55e',
/*
OpenSEO's own MCP server, handed to the agent as-is.
A PATH, resolved against this Officer — so the agent reaches `/api/openseo/mcp`, through the same
gate as everything else, and never the instance directly. Which matters more here than elsewhere:
upstream self-hosts unauthenticated by design, so the proxy is the authentication.
Not wrapped as Officer-native tools, deliberately. The vocabulary moves upstream; a copy would go
stale and fail silently, with a tool existing that Officer cannot see.
*/
mcp: { path: '/mcp' },
permissions: [
{
key: 'openseo',
label: 'OpenSEO',
description: 'Keyword research, rank tracking, competitor and backlink data, and site audits',
/*
The connection form, the probe, and the DataForSEO key.
Owner-only by PREFIX, so `/_config/test` is covered too. That matters here for the same reason it
does in proxy-manager: the test request carries a credential in its body, and a member who can
call it has an oracle pointed at somebody else's key.
*/
ownerOnly: ['/_config'],
},
],
};
export default manifest;