diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..6ef10e9 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,140 @@ +{ + "name": "interchange-plugin", + "interface": { + "displayName": "Apostra" + }, + "plugins": [ + { + "name": "apostra-developer", + "source": { + "source": "local", + "path": "./plugins/apostra-developer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "amc-listing", + "source": { + "source": "local", + "path": "./plugins/amc-listing" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "amc-merchandising", + "source": { + "source": "local", + "path": "./plugins/amc-merchandising" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "amc-distribution", + "source": { + "source": "local", + "path": "./plugins/amc-distribution" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Deployment" + }, + { + "name": "amc-campaign-management", + "source": { + "source": "local", + "path": "./plugins/amc-campaign-management" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "buyer-account-setup", + "source": { + "source": "local", + "path": "./plugins/buyer-account-setup" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "buyer-campaign-management", + "source": { + "source": "local", + "path": "./plugins/buyer-campaign-management" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "buyer-creative-management", + "source": { + "source": "local", + "path": "./plugins/buyer-creative-management" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "buyer-reporting", + "source": { + "source": "local", + "path": "./plugins/buyer-reporting" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Monitoring" + }, + { + "name": "sales-agent-testing", + "source": { + "source": "local", + "path": "./plugins/sales-agent-testing" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Testing" + }, + { + "name": "amc-self-serve-buyer", + "source": { + "source": "local", + "path": "./plugins/amc-self-serve-buyer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 14b88b4..c711b66 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,6 +7,29 @@ "email": "engineering@scope3.com" }, "plugins": [ + { + "name": "apostra-developer", + "description": "Plan, run, automate, and report on advertising across sellers, or build those jobs into an application.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "source": "./plugins/apostra-developer", + "category": "development", + "keywords": [ + "apostra", + "advertising", + "campaign automation", + "growth marketing", + "media buying", + "reporting", + "developer", + "mcp", + "agents" + ], + "homepage": "https://docs.interchange.io/v3/overview", + "license": "Apache-2.0" + }, { "name": "amc-listing", "description": "Build and maintain an Agentic Media Company listing in Interchange.", diff --git a/README.md b/README.md index 337a6c1..2b948c0 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,48 @@ -# Interchange plugin marketplace +# Apostra developer marketplace -Install only the workflow packages relevant to your role. Packages select -instructions and starter workflows; they do not grant account permissions. -Interchange OAuth and server-side entitlements remain authoritative. +Connect a coding agent to Apostra, verify the account, and build an advertising +workflow. Start with `apostra-developer`; install the narrower operating +packages only when you need them. Packages do not grant account permissions. +Apostra OAuth and server-side entitlements remain authoritative. -## Install +## Install in Codex + +```sh +codex plugin marketplace add scope3data/interchange-plugin +codex plugin add apostra-developer@interchange-plugin +``` + +The native plugin installs both the `build-with-apostra` skill and the Apostra +MCP connection. Complete OAuth when prompted, then ask Codex: "Verify my +Apostra account and help me build a seller discovery prototype." + +## Install in Claude Code ``` /plugin marketplace add scope3data/interchange-plugin -/plugin install buyer-campaign-management@interchange-plugin +/plugin install apostra-developer@interchange-plugin ``` Complete OAuth when prompted. Never paste API keys or provider credentials into a prompt. +## Install the skill in another compatible agent + +```sh +npx skills add scope3data/interchange-plugin \ + --skill build-with-apostra \ + --yes +``` + +This route installs the instructions only. Connect the agent's MCP client to +`https://api.interchange.io/mcp/v3`, complete OAuth, and run the read-only +`get_status` check before building. + ## Packages | Package | Purpose | Current canonical skills | |---|---|---| +| `apostra-developer` | Plan, run, automate, and report on advertising across sellers, or build those jobs into an application. | `build-with-apostra@1.0.0` | | `amc-listing` | Build and maintain an Agentic Media Company listing in Interchange. | MCP access; workflow skill forthcoming | | `amc-merchandising` | Merchandise an Agentic Media Company's inventory and products in Interchange. | MCP access; workflow skill forthcoming | | `amc-distribution` | Prepare and distribute an Agentic Media Company through supported agent channels. | `publish-an-openai-app@1.0.0` | @@ -37,8 +62,8 @@ Skill files are composed from the canonical, versioned Interchange skill registry. When a registry current-version pointer or referenced asset changes, the source workflow opens a reviewed sync PR for this repository. -Every durable change remains confirmation-gated by the underlying Interchange -tool contract. Full documentation lives at https://docs.interchange.io. +Every durable change remains confirmation-gated by the underlying Apostra +tool contract. Build guides live at https://docs.interchange.io/v3/overview. ## License diff --git a/SOURCE_DIGEST b/SOURCE_DIGEST index 0a5800c..7f02f6d 100644 --- a/SOURCE_DIGEST +++ b/SOURCE_DIGEST @@ -1 +1 @@ -689f2de18ba359a8c6faac22b53d6ad2fcb31320c66c135951733ae6b4a33d2a +2e4a53c3a0f1362322cb2dc398125123580d31198330264fa516f33a41cc5dfa diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 0000000..51760d9 --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,21 @@ +# Apostra Python starter + +This Python 3.11+ starter connects to Apostra's v3 MCP endpoint with a scoped +access token and makes one read-only `get_status` call. Connection, +verification, and session teardown share a 15-second deadline. The starter +cancels SDK tasks and closes HTTP connections before returning. On timeout, +check network access and the MCP endpoint, then retry. For browser OAuth, use the +[direct MCP quickstart](https://docs.interchange.io/v3/quickstart). + +```sh +python -m venv .venv +. .venv/bin/activate +pip install -r requirements.txt +APOSTRA_ACCESS_TOKEN='your-scoped-token' python main.py +python -m unittest test_main.py +``` + +Keep the token in an environment variable or secret manager. Do not commit it. +After verification works, follow the +[build-an-agent guide](https://docs.interchange.io/v3/build-an-agent) to add +tool discovery, durable checkpoints, confirmation gates, and retries. diff --git a/examples/python/main.py b/examples/python/main.py new file mode 100644 index 0000000..bd1057c --- /dev/null +++ b/examples/python/main.py @@ -0,0 +1,70 @@ +"""Verify an Apostra account through the v3 MCP endpoint.""" + +import asyncio +import json +import os +from typing import Any, Protocol + +import anyio + +IO_TIMEOUT_SECONDS = 15 + +APOSTRA_MCP_URL = "https://api.interchange.io/mcp/v3" + + +class ToolClient(Protocol): + async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: ... + + +async def verify_apostra_account( + client: ToolClient, timeout: float = IO_TIMEOUT_SECONDS +) -> Any: + """Make exactly one read-only call and fail closed on a tool error.""" + try: + result = await asyncio.wait_for(client.call_tool("get_status", {}), timeout) + except asyncio.TimeoutError as error: + raise RuntimeError( + "Apostra verification timed out. Check network access and retry." + ) from error + if result.isError or result.structuredContent is None: + raise RuntimeError("Apostra account verification failed") + return result.structuredContent + + +async def connect_and_verify(token: str, endpoint: str = APOSTRA_MCP_URL) -> Any: + from mcp import ClientSession + from mcp.client.streamable_http import streamablehttp_client + + headers = {"Authorization": f"Bearer {token}"} + async with streamablehttp_client(endpoint, headers=headers) as streams: + read_stream, write_stream, _ = streams + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + return await verify_apostra_account(session) + + +async def run_verification( + token: str, timeout: float = IO_TIMEOUT_SECONDS, + endpoint: str = APOSTRA_MCP_URL +) -> Any: + # Level cancellation reaches every SDK task and every teardown await. + # asyncio.wait_for only cancels once, so SDK cleanup could outlive it. + try: + with anyio.fail_after(timeout): + return await connect_and_verify(token, endpoint) + except asyncio.TimeoutError as error: + raise RuntimeError( + "Apostra connection or verification timed out. " + "Check network access and the MCP endpoint, then retry." + ) from error + + +async def main() -> None: + token = os.environ.get("APOSTRA_ACCESS_TOKEN") + if not token: + raise RuntimeError("Set APOSTRA_ACCESS_TOKEN before running") + print(json.dumps(await run_verification(token), indent=2)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt new file mode 100644 index 0000000..f1ee4ad --- /dev/null +++ b/examples/python/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.26,<2 +anyio>=4.5,<5 diff --git a/examples/python/test_main.py b/examples/python/test_main.py new file mode 100644 index 0000000..0b8e203 --- /dev/null +++ b/examples/python/test_main.py @@ -0,0 +1,194 @@ +import asyncio +import importlib +import json +import time +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +from main import run_verification, verify_apostra_account + + +class FakeClient: + def __init__(self, result): + self.result = result + self.calls = [] + + async def call_tool(self, name, arguments): + self.calls.append((name, arguments)) + return self.result + + +class VerifyAccountTest(unittest.IsolatedAsyncioTestCase): + async def test_verifies_with_one_read_only_call(self): + expected = {"activeAccount": {"id": "buyer-123", "kind": "buyer"}} + client = FakeClient( + SimpleNamespace(isError=False, structuredContent=expected) + ) + + status = await verify_apostra_account(client) + + self.assertEqual(status, expected) + self.assertEqual(client.calls, [("get_status", {})]) + + async def test_fails_closed_on_tool_error(self): + client = FakeClient(SimpleNamespace(isError=True, structuredContent=None)) + with self.assertRaisesRegex(RuntimeError, "verification failed"): + await verify_apostra_account(client) + + async def test_cancels_stalled_tool_call(self): + cancelled = asyncio.Event() + + class StalledClient: + async def call_tool(self, name, arguments): + try: + await asyncio.Event().wait() + finally: + cancelled.set() + + with self.assertRaisesRegex(RuntimeError, "verification timed out.*retry"): + await verify_apostra_account(StalledClient(), timeout=0.01) + self.assertTrue(cancelled.is_set()) + + async def test_cancels_stalled_connection(self): + cancelled = asyncio.Event() + + async def stalled_connect(*args): + try: + await asyncio.Event().wait() + finally: + cancelled.set() + + with ( + patch("main.connect_and_verify", stalled_connect), + self.assertRaisesRegex(RuntimeError, "connection or verification timed out.*retry"), + ): + await run_verification("local-test-token", timeout=0.01) + self.assertTrue(cancelled.is_set()) + + async def test_bounds_stalled_http_connection(self): + # Keep module import time outside the short network deadline. + importlib.import_module("mcp.client.streamable_http") + closed = asyncio.Event() + + async def stalled_server(reader, writer): + try: + await reader.read() + finally: + writer.close() + await writer.wait_closed() + closed.set() + + server = await asyncio.start_server(stalled_server, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + try: + with self.assertRaisesRegex(RuntimeError, "connection or verification timed out.*retry"): + await asyncio.wait_for( + run_verification("local-test-token", timeout=0.5, + endpoint=f"http://127.0.0.1:{port}/mcp"), + timeout=2, + ) + await asyncio.wait_for(closed.wait(), timeout=2) + finally: + server.close() + await server.wait_closed() + + async def assert_bounded_session(self, stall_tool): + importlib.import_module("mcp.client.streamable_http") + writers = set() + handlers = set() + methods = set() + baseline = asyncio.all_tasks() + + async def server_handler(reader, writer): + writers.add(writer) + handlers.add(asyncio.current_task()) + try: + while True: + header = await reader.readuntil(b"\r\n\r\n") + lines = header.decode().split("\r\n") + method = lines[0].split()[0] + methods.add(method) + headers = dict(line.lower().split(": ", 1) for line in lines[1:] if ": " in line) + body = await reader.readexactly(int(headers.get("content-length", 0))) + if method == "GET": + writer.write(b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n: waiting\n\n") + await writer.drain() + await reader.read() + return + if method == "DELETE": + await reader.read() + return + message = json.loads(body) + methods.add(message["method"]) + if message["method"] == "tools/call" and stall_tool: + await reader.read() + return + if "id" not in message: + writer.write(b"HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\n\r\n") + else: + if message["method"] == "initialize": + result = { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "local", "version": "1"}, + } + elif message["method"] == "tools/list": + result = {"tools": [{ + "name": "get_status", + "inputSchema": {"type": "object"}, + }]} + else: + result = {"content": [], "structuredContent": {"activeAccount": {"id": "local"}}} + payload = json.dumps({"jsonrpc": "2.0", "id": message["id"], "result": result}).encode() + writer.write(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nMcp-Session-Id: local-session\r\n" + + f"Content-Length: {len(payload)}\r\n\r\n".encode() + payload) + await writer.drain() + except (asyncio.IncompleteReadError, ConnectionError): + pass + finally: + writer.close() + await writer.wait_closed() + writers.discard(writer) + handlers.discard(asyncio.current_task()) + + server = await asyncio.start_server(server_handler, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + operation = asyncio.create_task(run_verification( + "local-test-token", timeout=0.25, endpoint=f"http://127.0.0.1:{port}/mcp")) + started = time.monotonic() + try: + # asyncio.wait observes the bound without trying to cancel a stuck cleanup. + done, _ = await asyncio.wait({operation}, timeout=0.75) + self.assertIn(operation, done, "public function exceeded its end-to-end bound") + with self.assertRaisesRegex(RuntimeError, "timed out.*retry"): + operation.result() + self.assertLess(time.monotonic() - started, 0.75) + self.assertIn("tools/call", methods) + self.assertIn("GET", methods) + if not stall_tool: + self.assertIn("DELETE", methods) + for _ in range(20): + await asyncio.sleep(0.01) + self.assertFalse(writers, "verification left network connections open") + self.assertFalse(asyncio.all_tasks() - baseline - handlers, + "verification left SDK tasks running") + finally: + for writer in list(writers): + writer.close() + if not operation.done(): + operation.cancel() + await asyncio.gather(operation, return_exceptions=True) + await asyncio.gather(*handlers, return_exceptions=True) + server.close() + await server.wait_closed() + + async def test_closes_stalled_tool_and_get_stream(self): + await self.assert_bounded_session(stall_tool=True) + + async def test_closes_stalled_delete_and_get_stream(self): + await self.assert_bounded_session(stall_tool=False) + + +if __name__ == "__main__": + unittest.main() diff --git a/examples/typescript/README.md b/examples/typescript/README.md new file mode 100644 index 0000000..81efc8c --- /dev/null +++ b/examples/typescript/README.md @@ -0,0 +1,19 @@ +# Apostra TypeScript starter + +This Node.js 20.18.1+ starter connects to Apostra's v3 MCP endpoint with a scoped +access token and makes one read-only `get_status` call. The 15-second deadline +covers connection, verification, and session teardown. The starter closes its +HTTP connections before returning. On timeout, check network access and the MCP +endpoint, then retry. For browser OAuth, use the +[direct MCP quickstart](https://docs.interchange.io/v3/quickstart). + +```sh +npm install +APOSTRA_ACCESS_TOKEN='your-scoped-token' npm start +npm test +``` + +Keep the token in an environment variable or secret manager. Do not commit it. +After verification works, follow the +[build-an-agent guide](https://docs.interchange.io/v3/build-an-agent) to add +tool discovery, durable checkpoints, confirmation gates, and retries. diff --git a/examples/typescript/package.json b/examples/typescript/package.json new file mode 100644 index 0000000..91e0be0 --- /dev/null +++ b/examples/typescript/package.json @@ -0,0 +1,20 @@ +{ + "name": "apostra-typescript-starter", + "private": true, + "type": "module", + "engines": { + "node": ">=20.18.1" + }, + "scripts": { + "start": "tsx src/main.ts", + "test": "tsx --test src/main.test.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "undici": "^7.29.1" + }, + "devDependencies": { + "tsx": "^4.20.6", + "typescript": "^6.0.3" + } +} diff --git a/examples/typescript/src/main.test.ts b/examples/typescript/src/main.test.ts new file mode 100644 index 0000000..3409ecb --- /dev/null +++ b/examples/typescript/src/main.test.ts @@ -0,0 +1,165 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:http"; +import type { Socket } from "node:net"; +import { setTimeout as delay } from "node:timers/promises"; +import test from "node:test"; + +import { + connectAndVerify, + verifyApostraAccount, + type ToolClient, +} from "./main.js"; + +test("verifies the account with one read-only call", async () => { + const calls: string[] = []; + const client: ToolClient = { + async callTool(input) { + calls.push(input.name); + return { + structuredContent: { + activeAccount: { id: "buyer-123", kind: "buyer" }, + }, + }; + }, + }; + + const status = await verifyApostraAccount(client); + + assert.deepEqual(calls, ["get_status"]); + assert.deepEqual(status, { + activeAccount: { id: "buyer-123", kind: "buyer" }, + }); +}); + +test("fails closed when get_status does not return structured content", async () => { + const client: ToolClient = { + async callTool() { + return { isError: true }; + }, + }; + + await assert.rejects( + verifyApostraAccount(client), + /Apostra account verification failed/, + ); +}); + +test("cancels a stalled verification call", async () => { + let signal: AbortSignal | undefined; + const client: ToolClient = { + callTool(_input, _schema, options) { + signal = options?.signal; + return new Promise(() => {}); + }, + }; + await assert.rejects(verifyApostraAccount(client, 20), /timed out.*retry/); + assert.equal(signal?.aborted, true); +}); + +test("bounds a stalled HTTP connection", { timeout: 2000 }, async () => { + const server = createServer(() => {}); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + try { + await assert.rejects( + connectAndVerify( + "local-test-token", + 50, + `http://127.0.0.1:${address.port}/mcp`, + ), + /timed out.*retry/, + ); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + } +}); + +for (const stallTool of [true, false]) { + test( + `closes all HTTP connections when ${stallTool ? "tools/call" : "DELETE teardown"} stalls`, + { timeout: 3000 }, + async () => { + const sockets = new Set(); + const methods = new Set(); + const server = createServer(async (request, response) => { + methods.add(request.method ?? ""); + if (request.method === "GET") { + response.writeHead(200, { "content-type": "text/event-stream" }); + response.write(": waiting\n\n"); + return; + } + if (request.method === "DELETE") return; + let body = ""; + for await (const chunk of request) body += chunk; + const message = JSON.parse(body); + methods.add(message.method); + if (message.method === "tools/call" && stallTool) return; + if (message.id === undefined) { + response.writeHead(202).end(); + return; + } + if (message.method === "initialize" && stallTool) await delay(120); + response.writeHead(200, { + "content-type": "application/json", + "mcp-session-id": "local-session", + }); + response.end( + JSON.stringify({ + jsonrpc: "2.0", + id: message.id, + result: + message.method === "initialize" + ? { + protocolVersion: "2025-03-26", + capabilities: { tools: {} }, + serverInfo: { name: "local", version: "1" }, + } + : { + content: [], + structuredContent: { activeAccount: { id: "local" } }, + }, + }), + ); + }); + server.on("connection", (socket) => { + sockets.add(socket); + socket.on("close", () => sockets.delete(socket)); + }); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + const started = performance.now(); + try { + await assert.rejects( + connectAndVerify( + "local-test-token", + 250, + `http://127.0.0.1:${address.port}/mcp`, + ), + /timed out.*retry/, + ); + assert.ok( + performance.now() - started < 750, + "public function exceeded its end-to-end bound", + ); + assert.ok(methods.has("tools/call")); + assert.ok(methods.has("GET")); + if (!stallTool) assert.ok(methods.has("DELETE")); + // Allow the server to observe remote socket closure, without closing it ourselves. + for (let i = 0; i < 10; i++) await delay(5); + assert.equal( + sockets.size, + 0, + "verification left network connections open", + ); + } finally { + for (const socket of sockets) socket.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + } + }, + ); +} diff --git a/examples/typescript/src/main.ts b/examples/typescript/src/main.ts new file mode 100644 index 0000000..3686551 --- /dev/null +++ b/examples/typescript/src/main.ts @@ -0,0 +1,129 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Agent } from "undici"; + +export const IO_TIMEOUT_MS = 15_000; + +export const APOSTRA_MCP_URL = "https://api.interchange.io/mcp/v3"; + +export interface ToolClient { + callTool( + input: { name: string; arguments: Record }, + schema?: undefined, + options?: { signal: AbortSignal; timeout: number }, + ): Promise>; +} + +export async function verifyApostraAccount( + client: ToolClient, + timeoutMs = IO_TIMEOUT_MS, +): Promise { + const result = await withDeadline( + (signal) => + client.callTool({ name: "get_status", arguments: {} }, undefined, { + signal, + timeout: timeoutMs, + }), + timeoutMs, + ); + if (result.isError || result.structuredContent === undefined) { + throw new Error("Apostra account verification failed"); + } + return result.structuredContent; +} + +async function withDeadline( + operation: (signal: AbortSignal) => Promise, + timeoutMs: number, +): Promise { + const controller = new AbortController(); + let timer: ReturnType | undefined; + const deadline = new Promise((_, reject) => { + timer = setTimeout(() => { + const error = new Error( + "Apostra connection or verification timed out. Check network access and the MCP endpoint, then retry.", + ); + controller.abort(error); + reject(error); + }, timeoutMs); + }); + try { + return await Promise.race([operation(controller.signal), deadline]); + } finally { + clearTimeout(timer); + } +} + +export async function connectAndVerify( + token: string, + timeoutMs = IO_TIMEOUT_MS, + endpoint = APOSTRA_MCP_URL, +): Promise { + const url = new URL(endpoint); + const lifetime = new AbortController(); + const timeoutError = new Error( + "Apostra connection or verification timed out. Check network access and the MCP endpoint, then retry.", + ); + const timer = setTimeout(() => lifetime.abort(timeoutError), timeoutMs); + const pending = new Set>(); + const dispatcher = new Agent(); + const client = new Client({ + name: "apostra-typescript-starter", + version: "1.0.0", + }); + const transport = new StreamableHTTPClientTransport(url, { + requestInit: { headers: { Authorization: `Bearer ${token}` } }, + // The SDK replaces requestInit.signal. Bind the lifetime at the actual + // fetch boundary so POST, GET, DELETE and cancellation notices all stop. + fetch: (url, init) => { + const options: RequestInit & { dispatcher: Agent } = { + ...init, + dispatcher, + signal: AbortSignal.any([ + lifetime.signal, + ...(init?.signal ? [init.signal] : []), + ]), + }; + const request = fetch(url, options); + pending.add(request); + void request.then( + () => pending.delete(request), + () => pending.delete(request), + ); + return request; + }, + }); + try { + await client.connect(transport, { + signal: lifetime.signal, + timeout: timeoutMs, + }); + const status = await verifyApostraAccount(client, timeoutMs); + await transport.terminateSession(); + return status; + } catch (error) { + if (lifetime.signal.aborted) throw timeoutError; + throw error; + } finally { + lifetime.abort(); + clearTimeout(timer); + try { + await client.close(); + } finally { + // Own the pool: aborting fetch alone can leave replacement/idle sockets. + // destroy() closes them locally, without waiting for the remote peer. + await dispatcher.destroy(); + await Promise.allSettled(pending); + } + } +} + +async function main(): Promise { + const token = process.env.APOSTRA_ACCESS_TOKEN; + if (!token) throw new Error("Set APOSTRA_ACCESS_TOKEN before running"); + console.log(JSON.stringify(await connectAndVerify(token), null, 2)); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await main(); +} diff --git a/plugins/amc-campaign-management/.claude-plugin/plugin.json b/plugins/amc-campaign-management/.claude-plugin/plugin.json index edf50c1..9bec515 100644 --- a/plugins/amc-campaign-management/.claude-plugin/plugin.json +++ b/plugins/amc-campaign-management/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "amc-campaign-management", "displayName": "AMC Campaign Management", "description": "Operate seller-side campaigns and delivery in Interchange.", - "version": "1.0.0+8956d0aeaf6e", + "version": "1.0.0+ab6f45c1d871", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/amc-campaign-management/.codex-plugin/plugin.json b/plugins/amc-campaign-management/.codex-plugin/plugin.json new file mode 100644 index 0000000..64583ec --- /dev/null +++ b/plugins/amc-campaign-management/.codex-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "amc-campaign-management", + "version": "1.0.0+ab6f45c1d871", + "description": "Operate seller-side campaigns and delivery in Interchange.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "campaigns", + "delivery", + "mcp" + ], + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "AMC Campaign Management", + "shortDescription": "Operate seller-side campaigns and delivery in Interchange.", + "longDescription": "Operate seller-side campaigns and delivery in Interchange.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use AMC Campaign Management with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/amc-distribution/.claude-plugin/plugin.json b/plugins/amc-distribution/.claude-plugin/plugin.json index 33d1433..13d273c 100644 --- a/plugins/amc-distribution/.claude-plugin/plugin.json +++ b/plugins/amc-distribution/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "amc-distribution", "displayName": "AMC Distribution", "description": "Prepare and distribute an Agentic Media Company through supported agent channels.", - "version": "1.0.0+5156e005ef0d", + "version": "1.0.0+886dc10e672b", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/amc-distribution/.codex-plugin/plugin.json b/plugins/amc-distribution/.codex-plugin/plugin.json new file mode 100644 index 0000000..a90f617 --- /dev/null +++ b/plugins/amc-distribution/.codex-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "amc-distribution", + "version": "1.0.0+886dc10e672b", + "description": "Prepare and distribute an Agentic Media Company through supported agent channels.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "distribution", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "AMC Distribution", + "shortDescription": "Prepare and distribute an Agentic Media Company through supported agent channels.", + "longDescription": "Prepare and distribute an Agentic Media Company through supported agent channels.", + "developerName": "Scope3 PBC", + "category": "Deployment", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use AMC Distribution with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/amc-listing/.claude-plugin/plugin.json b/plugins/amc-listing/.claude-plugin/plugin.json index c8f539a..da12937 100644 --- a/plugins/amc-listing/.claude-plugin/plugin.json +++ b/plugins/amc-listing/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "amc-listing", "displayName": "AMC Listing", "description": "Build and maintain an Agentic Media Company listing in Interchange.", - "version": "1.0.0+170e8d2436ba", + "version": "1.0.0+4b0705da1792", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/amc-listing/.codex-plugin/plugin.json b/plugins/amc-listing/.codex-plugin/plugin.json new file mode 100644 index 0000000..035af5d --- /dev/null +++ b/plugins/amc-listing/.codex-plugin/plugin.json @@ -0,0 +1,34 @@ +{ + "name": "amc-listing", + "version": "1.0.0+4b0705da1792", + "description": "Build and maintain an Agentic Media Company listing in Interchange.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "listing", + "mcp" + ], + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "AMC Listing", + "shortDescription": "Build and maintain an Agentic Media Company listing in Interchange.", + "longDescription": "Build and maintain an Agentic Media Company listing in Interchange.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use AMC Listing with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/amc-merchandising/.claude-plugin/plugin.json b/plugins/amc-merchandising/.claude-plugin/plugin.json index ca2caf7..8f64055 100644 --- a/plugins/amc-merchandising/.claude-plugin/plugin.json +++ b/plugins/amc-merchandising/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "amc-merchandising", "displayName": "AMC Merchandising", "description": "Merchandise an Agentic Media Company's inventory and products in Interchange.", - "version": "1.0.0+6128c7645abd", + "version": "1.0.0+0630ecb58bb1", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/amc-merchandising/.codex-plugin/plugin.json b/plugins/amc-merchandising/.codex-plugin/plugin.json new file mode 100644 index 0000000..cae8e5d --- /dev/null +++ b/plugins/amc-merchandising/.codex-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "amc-merchandising", + "version": "1.0.0+0630ecb58bb1", + "description": "Merchandise an Agentic Media Company's inventory and products in Interchange.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "inventory", + "products", + "mcp" + ], + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "AMC Merchandising", + "shortDescription": "Merchandise an Agentic Media Company's inventory and products in Interchange.", + "longDescription": "Merchandise an Agentic Media Company's inventory and products in Interchange.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use AMC Merchandising with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/amc-self-serve-buyer/.claude-plugin/plugin.json b/plugins/amc-self-serve-buyer/.claude-plugin/plugin.json index 5591a9b..8a8cc64 100644 --- a/plugins/amc-self-serve-buyer/.claude-plugin/plugin.json +++ b/plugins/amc-self-serve-buyer/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "amc-self-serve-buyer", "displayName": "AMC Self-Serve Buyer", "description": "Bundle buyer workflows for an Agentic Media Company's own self-serve plugin.", - "version": "1.0.0+78467a017992", + "version": "1.0.0+1af684d1fcc6", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/amc-self-serve-buyer/.codex-plugin/plugin.json b/plugins/amc-self-serve-buyer/.codex-plugin/plugin.json new file mode 100644 index 0000000..c0c7557 --- /dev/null +++ b/plugins/amc-self-serve-buyer/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "amc-self-serve-buyer", + "version": "1.0.0+1af684d1fcc6", + "description": "Bundle buyer workflows for an Agentic Media Company's own self-serve plugin.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "buyer", + "self-serve", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "AMC Self-Serve Buyer", + "shortDescription": "Bundle buyer workflows for an Agentic Media Company's own self-serve plugin.", + "longDescription": "Bundle buyer workflows for an Agentic Media Company's own self-serve plugin.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use AMC Self-Serve Buyer with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/apostra-developer/.claude-plugin/plugin.json b/plugins/apostra-developer/.claude-plugin/plugin.json new file mode 100644 index 0000000..935dd54 --- /dev/null +++ b/plugins/apostra-developer/.claude-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "apostra-developer", + "displayName": "Apostra Developer", + "description": "Plan, run, automate, and report on advertising across sellers, or build those jobs into an application.", + "version": "1.0.0+01db25a53c32", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io/v3/overview", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "apostra", + "advertising", + "campaign automation", + "growth marketing", + "media buying", + "reporting", + "developer", + "mcp", + "agents" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/" +} diff --git a/plugins/apostra-developer/.codex-plugin/plugin.json b/plugins/apostra-developer/.codex-plugin/plugin.json new file mode 100644 index 0000000..9520a1c --- /dev/null +++ b/plugins/apostra-developer/.codex-plugin/plugin.json @@ -0,0 +1,43 @@ +{ + "name": "apostra-developer", + "version": "1.0.0+01db25a53c32", + "description": "Plan, run, automate, and report on advertising across sellers, or build those jobs into an application.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io/v3/overview", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "apostra", + "advertising", + "campaign automation", + "growth marketing", + "media buying", + "reporting", + "developer", + "mcp", + "agents" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Apostra Developer", + "shortDescription": "Connect Apostra and build a tested integration.", + "longDescription": "Plan, run, automate, and report on advertising across sellers, or build those jobs into an application.", + "developerName": "Scope3 PBC", + "category": "Development", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io/v3/overview", + "defaultPrompt": [ + "Connect this repository to Apostra and build a tested seller discovery prototype." + ], + "brandColor": "#7513D4", + "composerIcon": "./assets/apostra-icon-purple.svg", + "logo": "./assets/apostra-icon-purple.svg" + } +} diff --git a/plugins/apostra-developer/.mcp.json b/plugins/apostra-developer/.mcp.json new file mode 100644 index 0000000..613afa9 --- /dev/null +++ b/plugins/apostra-developer/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "interchange": { + "type": "http", + "url": "https://api.interchange.io/mcp/v3" + } + } +} diff --git a/plugins/apostra-developer/assets/apostra-icon-purple.svg b/plugins/apostra-developer/assets/apostra-icon-purple.svg new file mode 100644 index 0000000..3b2c9e5 --- /dev/null +++ b/plugins/apostra-developer/assets/apostra-icon-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/apostra-developer/assets/composer-icon.png b/plugins/apostra-developer/assets/composer-icon.png new file mode 100644 index 0000000..89d1cba Binary files /dev/null and b/plugins/apostra-developer/assets/composer-icon.png differ diff --git a/plugins/apostra-developer/assets/directory-icon.png b/plugins/apostra-developer/assets/directory-icon.png new file mode 100644 index 0000000..990566c Binary files /dev/null and b/plugins/apostra-developer/assets/directory-icon.png differ diff --git a/plugins/apostra-developer/skills/build-with-apostra/SKILL.md b/plugins/apostra-developer/skills/build-with-apostra/SKILL.md new file mode 100644 index 0000000..4c25dd0 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/SKILL.md @@ -0,0 +1,93 @@ +--- +name: build-with-apostra +description: Use Apostra to plan or run ads, find places to advertise, automate campaign work, read results, or build those jobs into software. Use when a marketer or developer asks for advertising help, an Apostra integration, API or MCP setup, authentication, reporting, or an AdCP sales-agent test. +--- + +# Build with Apostra + +Help the person reach a useful advertising outcome or a working, tested +integration. They may describe the job as running ads, finding publishers, +automating campaign work, or getting results into a report. Do not require them +to know terms such as advertising agent, seller discovery, orchestration, or +MCP. + +Apostra provides the account, campaign, seller connection, execution, and +reporting state behind the work. Read the [developer docs](https://docs.interchange.io/v3/overview) +for the product model; use live documentation and the connected account's +`tools/list` schemas for capabilities and exact call shapes. + +## Start in the repository + +Inspect the project's instructions, language, package manager, existing MCP +configuration, authentication approach, and test commands. Reuse its conventions. +Identify the requested outcome and choose the smallest working example that +demonstrates it. Do not create a new framework or replace a working integration +merely to match a starter. + +Choose how much of the operating loop the developer wants to own: + +- **Managed workflow:** work interactively through Apostra and its connected + assistant; keep campaign and delivery state in Apostra. This does not promise + a hosted runtime for arbitrary customer code. +- **Composable workflow:** the application owns decisions, scheduling, and + recovery and calls Apostra for the required operations and data. +- **Hybrid workflow:** application code owns durable checkpoints and approvals; + an agent handles judgment within those boundaries. Prefer this for a durable + buyer agent unless the developer has chosen another architecture. + +For agent-facing work, use a direct MCP connection to +`https://api.interchange.io/mcp/v3` with the host's OAuth flow. Native +`apostra-developer` plugin installation is pending mirror publication and +fresh-install verification; use direct MCP until both are complete. +For deterministic server or ETL work that needs a REST contract or an +operation V3 does not expose, use the documented stable V2 REST API. Explain +that choice in terms of the requested job, not a version-selection exercise. + +## Connect and prove access + +Read [connection.md](references/connection.md) when installing, authenticating, +or changing account context. Complete read-only verification before any remote +write. For MCP, call `get_status`, inspect the current tools, and read the +intended account resource. For a REST-only integration, use the documented +authorized resource read. Report the observed account and any readiness blocker. + +A local skill installation is not an MCP connection. OAuth success is not +permission to buy. A passing fixture test is not a successful live API call. +Keep those outcomes separate throughout the work. + +## Build the requested workflow + +Read only the reference needed for the task: + +- [agents.md](references/agents.md): seller discovery, connecting a seller, + durable buyer-agent loops, and independent agent identity. +- [data-pipelines.md](references/data-pipelines.md): event ingestion handoff, + aggregate reporting, exports, and scheduled delivery to cloud storage. +- [sales-agent-testing.md](references/sales-agent-testing.md): owned-supply + discovery and sandbox transaction tests with cleanup. + +Before following a workflow that changes account state, retrieve it through +`get({ kind: "skill", id: "build-with-apostra" })` in the active account. +An installed copy does not establish account eligibility. Retrieve a more +specific workflow the same way before executing its mutations. + +Use returned identifiers, revisions, continuation cursors, and documented +idempotency semantics. Preserve customer approvals and each tool's confirmation +boundary. A request to build an integration does not authorize launching a +campaign, accepting terms, granting cloud access, or publishing an app. + +## Verify and hand off + +Run the generated code's deterministic test before calling the local example +working. Exercise meaningful behavior such as account denial, tool errors, +pagination, or restart recovery. Use synthetic fixtures and keep live writes +out of the default test command. If authentication is blocked, continue the +local example and state that live verification remains incomplete. + +Report the chosen architecture, files created, test command and observed +result, read-only live result, and the next runnable step. Never report OAuth, +account access, ingestion, delivery, or launch from an inferred outcome. +When collecting activation evidence, record bounded outcomes and timestamps; +exclude prompts, source code, credentials, payloads, and signed URLs. Follow +the [skill correlation contract](https://docs.interchange.io/v2/skill#correlate-a-skill-run) +when the host supports it; correlation metadata does not prove completion. diff --git a/plugins/apostra-developer/skills/build-with-apostra/agents/openai.yaml b/plugins/apostra-developer/skills/build-with-apostra/agents/openai.yaml new file mode 100644 index 0000000..3a429a3 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/agents/openai.yaml @@ -0,0 +1,12 @@ +interface: + display_name: "Build with Apostra" + short_description: "Connect Apostra and build a tested integration" + default_prompt: "Use $build-with-apostra to connect this repository to Apostra and build a tested seller discovery prototype." + +dependencies: + tools: + - type: "mcp" + value: "interchange" + description: "Apostra account-resolved MCP server" + transport: "streamable_http" + url: "https://api.interchange.io/mcp/v3" diff --git a/plugins/apostra-developer/skills/build-with-apostra/manifest.json b/plugins/apostra-developer/skills/build-with-apostra/manifest.json new file mode 100644 index 0000000..df63746 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/manifest.json @@ -0,0 +1,119 @@ +{ + "schemaVersion": "3.0", + "id": "build-with-apostra", + "name": "Build with Apostra", + "version": "1.0.0", + "description": "Use Apostra to plan or run ads, automate campaign work, read results, or build those jobs into tested software.", + "steward": { + "owner": "bokelley" + }, + "entrypoint": "SKILL.md", + "assets": [ + { + "route": "SKILL.md", + "file": "SKILL.md", + "contentType": "text/markdown; charset=utf-8" + }, + { + "route": "manifest.json", + "file": "manifest.json", + "contentType": "application/json; charset=utf-8" + }, + { + "route": "agents/openai.yaml", + "file": "agents/openai.yaml", + "contentType": "application/yaml; charset=utf-8" + }, + { + "route": "references/connection.md", + "file": "references/connection.md", + "contentType": "text/markdown; charset=utf-8" + }, + { + "route": "references/agents.md", + "file": "references/agents.md", + "contentType": "text/markdown; charset=utf-8" + }, + { + "route": "references/data-pipelines.md", + "file": "references/data-pipelines.md", + "contentType": "text/markdown; charset=utf-8" + }, + { + "route": "references/sales-agent-testing.md", + "file": "references/sales-agent-testing.md", + "contentType": "text/markdown; charset=utf-8" + }, + { + "route": "references/scenarios.json", + "file": "references/scenarios.json", + "contentType": "application/json; charset=utf-8" + } + ], + "applicability": { + "audiences": [ + { + "accountKind": "buyer", + "clientChannels": ["standard"], + "requiredCapabilities": [], + "requiredFeatureEntitlements": [] + }, + { + "accountKind": "seller", + "clientChannels": ["standard"], + "requiredCapabilities": [], + "requiredFeatureEntitlements": [] + } + ] + }, + "distribution": { + "mcpAccountKinds": [], + "submissionBundleAccountKinds": [] + }, + "tools": { + "required": ["get_status", "search", "get"], + "optional": [ + "switch_account", + "save_buyer_agent", + "save_connection", + "save_advertiser", + "save_campaign", + "save_media_buy", + "save_measurement_source", + "request_proposals", + "get_delivery" + ] + }, + "documentation": [ + "mintlify/v3/overview.mdx", + "mintlify/v3/choose-your-loop.mdx", + "mintlify/v3/quickstart.mdx", + "mintlify/v3/build-an-agent.mdx", + "mintlify/v3/bring-data-in.mdx", + "mintlify/v3/reporting-pipeline.mdx", + "mintlify/v2/setup/v3/buyer-workflows.mdx", + "mintlify/v2/setup/v3/seller-workflows.mdx", + "mintlify/v2/setup/buyer-agent-credentials.mdx", + "mintlify/v2/features/sandbox.mdx" + ], + "evaluation": { + "acceptance": { + "scenarioAsset": "references/scenarios.json", + "scenarioIds": [ + "install-and-discover", + "instructions-without-connector", + "durable-buyer-agent", + "reporting-pipeline", + "event-ingestion-handoff", + "sales-agent-validation" + ] + }, + "telemetry": { + "correlationSkillId": "build-with-apostra@1.0.0", + "outcomeSignals": [ + "workflow_terminal_state", + "scenario_assertion_results" + ] + } + } +} diff --git a/plugins/apostra-developer/skills/build-with-apostra/references/agents.md b/plugins/apostra-developer/skills/build-with-apostra/references/agents.md new file mode 100644 index 0000000..5c27ed1 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/references/agents.md @@ -0,0 +1,93 @@ +# Build an application or agent + +Read [build an agent](https://docs.interchange.io/v3/build-an-agent) +and the [buyer workflow](https://docs.interchange.io/v2/setup/v3/buyer-workflows) +for the requested part of the loop. Apostra keeps the canonical campaign, +transaction, and delivery state. The application may keep IDs, approvals, +checkpoints, and its own data without rebuilding seller-specific adapters. + +## Seller discovery prototype + +Build a read-only slice first: + +1. Complete connection verification and retain the account kind. +2. Inspect the current `search` schema and search `kind: "seller"` with the + developer's query. Preserve each returned seller ID and name. +3. Follow the returned pagination contract within an explicit result/page + bound. Distinguish no matches, partial results, account denial, and a tool + error. Never turn an MCP `isError` result into a successful empty list. +4. Present the returned records in the project's existing CLI or UI. Keep the + MCP transport behind a small interface so fixture tests need no account. +5. Run a deterministic test covering successful discovery and a meaningful + failure or continuation case. Then run the read-only live example when + credentials are available and report its result separately. + +Discovery does not require creating an advertiser, campaign, seller connection, +or media buy. Requesting live proposals is a later workflow that requires its +own campaign context; do not smuggle it into this prototype. + +The public TypeScript and Python starters are pending mirror publication and +fresh-install verification. Build the read-only example using the +[direct MCP connection](connection.md) and current runtime schema. +Adapt the example to the repository's package manager and test runner. Do not +overwrite existing files to reproduce a blank-repository example. + +## Connect a seller + +First establish whether the developer means connecting a buyer to an existing +seller or publishing their own inventory. Read the applicable +[buyer workflow](https://docs.interchange.io/v2/setup/v3/buyer-workflows) or +[seller workflow](https://docs.interchange.io/v2/setup/v3/seller-workflows). + +Resolve an existing seller through the authenticated catalog and inspect the +returned connection/readiness state. Follow the specific supported connection +action and human authorization handoff. A website URL or guessed ID is not a +seller identity, provider grant, or execution-ready connection. For owned +inventory, follow the seller's source, product, and publication workflow rather +than creating a buyer connection as a substitute. + +## Durable buyer-agent loop + +Choose the hybrid model when an agent makes decisions but the application +must resume after a process restart: + +1. Keep the advertiser, draft campaign, discovery execution, product query, + selected offers, creative versions, media buys, and returned revisions in a + durable store. Store approval records alongside the exact object revision. +2. Drive the workflow from fresh reads. Use the current catalog and buyer + guide for typed saves and `request_proposals`; preserve returned qualified + identifiers without reconstructing them. +3. Persist an asynchronous request's returned handle before scheduling a + continuation. Poll that execution and follow result cursors within bounded + retries. A restart resumes the saved operation; it does not create another + proposal round with a fresh key. +4. Make the application enforce the confirmation boundary. Preview campaign + launch with `save_campaign`, present the returned launch plan, and obtain + the authorized user's decision before the separate confirmation call. Use + the returned revision and each call's documented idempotency semantics. +5. On a conflict, timeout, or partial-write response, re-read persisted state + before deciding whether to retry. Do not infer launch from staging or from + a transport-level success. +6. Query delivery for an explicit scope and window. Keep Apostra report and + object references in the orchestrator; build a separate warehouse only + when the developer's data requirements call for one. + +Test a restart between scheduling and completion, duplicate delivery of a +workflow event, and a changed revision between preview and confirmation. +Tests must prove the application does not duplicate the operation or launch +an unapproved revision. Default tests use fixtures and never buy media. + +## Give independent software an identity + +An interactive coding agent using a user's connection is not automatically an +installed buyer agent. Register only when the software needs its own identity, +credentials, advertiser grants, and lifecycle. + +Read [manage buyer agents](https://docs.interchange.io/v2/setup/buyer-agent-credentials) +before implementing registration or access. Where advertised by the current +catalog, `save_buyer_agent` supports external-agent management under its +direct-administrator boundary. Use the documented console or REST path for +controls absent from V3. Keep identity creation, credential provisioning, +advertiser grants, and machine-notification setup as separate outcomes. +Never put one-time credential secrets in chat, generated source, or test +fixtures. Do not promise a hosted runtime merely because registration exists. diff --git a/plugins/apostra-developer/skills/build-with-apostra/references/connection.md b/plugins/apostra-developer/skills/build-with-apostra/references/connection.md new file mode 100644 index 0000000..f2b6693 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/references/connection.md @@ -0,0 +1,65 @@ +# Connect, authenticate, and verify + +Read the public [quickstart](https://docs.interchange.io/v3/quickstart) and +[MCP client setup](https://docs.interchange.io/v3/client-setup) for current +host-specific commands. Keep the public repository and protocol identifiers +spelled as published even when they retain `interchange` in their names. + +## Interactive coding agent + +1. Inspect existing plugins and remote MCP connections. Reuse an existing + working V3 connection when it serves the intended account. +2. Set up a direct MCP connection using the host's supported remote streamable + HTTP configuration: `https://api.interchange.io/mcp/v3`. If the skill was + installed through `npx skills`, this connection is still a separate step. + Skill dependencies describe the connection; do not assume the host + automatically installed it. +3. Native `apostra-developer` plugin installation from the + `scope3data/interchange-plugin` marketplace is pending mirror publication + and fresh-install verification. Use direct MCP until both are complete; + do not recommend installing the unpublished package. +4. Let the host perform OAuth discovery and credential storage. Do not ask + the user to paste a token into chat or a repository file. +5. Call `get_status` first. Record the active account ID, account kind, + readiness, and returned next actions without printing credential material. +6. Inspect `tools/list`. Use a returned account ID if an account switch is + needed, then call `get_status` again. An account ID is not a seller ID. +7. Make one scoped read appropriate to the task. A buyer's seller discovery + prototype can use `search` with `kind: "seller"`; a seller integration can + start with its inventory sources. Use the current schema and retain the + structured response. An empty result is a valid read, not sample data. + +Do not modify billing, connections, campaigns, or account settings to make an +installation check pass. Explain a returned readiness blocker and proceed +only with the work the account can perform. If OAuth cannot complete in the +current host, give the exact connection action and finish locally testable work. + +## Deployed application + +Read [authentication](https://docs.interchange.io/v3/authentication) and its +[credential reference](https://docs.interchange.io/v2/authentication) before +selecting credentials. Choose supported user authorization or a documented +backend credential based on who the software represents. Store secrets in the +deployment's secret manager and expose only environment-variable names in +the project. + +The interactive MCP OAuth token is bound to the exact MCP resource. Do not +copy it into a REST client or treat it as a deployable application credential. +Backend OAuth client credentials and an independently registered buyer agent +have different authority; consult the agent-management guide when independent +identity is required. Registration alone grants no advertiser access. + +For a REST-only job, verify its credential with a documented read on the exact +account resource before any write. Do not force a second MCP integration into +an otherwise deterministic REST service only to run `get_status`. + +## Retrieve facts as needed + +With MCP available, find the relevant public documentation using +`search({ sources: ["docs"], query: "..." })`. Read the selected `document` +through that same tool before implementing its contract. Use returned citation +URLs in the handoff. Without MCP, use the public pages directly. + +Do not invent a `/api/v3` equivalent of an MCP tool. Runtime schemas determine +which operations the active account can call; a documentation match, visible +tool, or successful login does not grant authority. diff --git a/plugins/apostra-developer/skills/build-with-apostra/references/data-pipelines.md b/plugins/apostra-developer/skills/build-with-apostra/references/data-pipelines.md new file mode 100644 index 0000000..86d67a5 --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/references/data-pipelines.md @@ -0,0 +1,82 @@ +# Bring data in and get data out + +Identify the source system, destination, advertiser, data shape, and expected +freshness before choosing an API. Reuse an existing scheduler, warehouse, or +secret manager. The integration's local tests should validate mapping and +retry behavior without sending customer data. + +## Event ingestion handoff + +Read [bring data in](https://docs.interchange.io/v3/bring-data-in) and retrieve +the applicable `set-up-an-event-source` skill through the active account's +`get` tool before source mutations. + +1. Read the advertiser's measurement sources and reuse a matching source + when appropriate. Native provider source IDs must come from that provider's + authorized connection; a partner name alone does not establish a connector. +2. Use the supported source-configuration workflow. Treat its returned + `setup` object as an installation handoff, not proof of a running sender. +3. Build the sender against the documented ingestion contract. Per-event + records and pre-aggregated measurement outcomes have different endpoints; + select by data shape, not by which example is shortest. Use V2 REST for + runtime event ingestion where the V3 catalog has no equivalent. +4. Define stable event IDs, time-zone handling, field mapping, bounded + retries, and deduplication. Test the mapping with synthetic events and + verify partial failures are surfaced rather than counted as accepted. +5. When a live test is authorized, use the documented test-event mechanism, + inspect its receipt, and read the source health. Preserve the difference + between configuration saved, test accepted, and production events flowing. +6. Attach conversion tracking to a campaign only when requested. Use the + source's raw `eventSourceId` for the campaign goal; the `event:`-prefixed + measurement-source noun ID addresses a different object. + +Do not send production events to prove installation or claim that `not_seen` +means a source is unusable. Return the sender's next command and the exact +observable receipt or health state still needed. + +## Reporting pipeline + +Read [build a reporting pipeline](https://docs.interchange.io/v3/reporting-pipeline) +and its linked contract guides. Choose the +output the developer actually needs: + +| Need | Integration to build | +| --- | --- | +| An agent answers a campaign-performance question | V3 `get_delivery` with explicit metrics, dimensions, window, and scope | +| A backend pulls aggregate metrics on a schedule | The documented V2 reporting REST contract and the application's scheduler | +| A one-off aggregate file | The documented CSV export; fetch its signed URL through the consuming backend | +| Recurring log-level data in owned cloud storage | Data Delivery credential and Output configuration with an observed destination probe | + +For agent reports, read the current `get_delivery` schema and +[delivery workflow](https://docs.interchange.io/v2/setup/v3/buyer-workflows#7-query-campaign-delivery). +Follow pagination and preserve unavailable metrics, currency, and period +information. Do not reinterpret missing data as zero or sum incompatible +currencies. Seller-reported delivery and independently measured events answer +different questions; name the source in the result. + +For a scheduled aggregate pull, implement this small pipeline: + +1. Resolve and read the exact authorized advertiser or campaign. +2. Choose explicit UTC date windows and a repeatable output key that includes + scope, date, dimensions, and currency as appropriate for the returned shape. +3. Validate each response and upsert the same window on retry. Delivery can + arrive or be corrected later, so choose and document a bounded lookback + instead of permanently closing yesterday after its first read. +4. Advance the checkpoint only after the destination write succeeds. Test a + repeated window and a destination failure to prove recovery does not lose + or double-count rows. +5. Record last successful fetch, source period, and destination write time. + An empty response alone does not prove the source is current. + +For recurring Data Delivery, use the documented cloud-access handoff, read +current configuration, preserve unrelated entries in full-replacement arrays, +register the destination, observe its validation status, and configure the +requested Outputs. The cloud owner grants IAM access. A validated probe proves +destination access; report the pipeline as delivering only after the intended +output is observed. Do not invent an empty file when no source data exists. + +Keep signed export URLs and cloud credentials out of logs, chat, fixtures, and +screenshots. A reporting export is sensitive data even after it is downloaded. +Machine notifications can wake an application where supported; they do not +replace the reporting records. Retrieve the relevant notification guide before +implementing a callback rather than inventing a generic V3 webhook tool. diff --git a/plugins/apostra-developer/skills/build-with-apostra/references/sales-agent-testing.md b/plugins/apostra-developer/skills/build-with-apostra/references/sales-agent-testing.md new file mode 100644 index 0000000..f6e7f1c --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/references/sales-agent-testing.md @@ -0,0 +1,43 @@ +# Test a sales agent + +Use the [seller workflow](https://docs.interchange.io/v2/setup/v3/seller-workflows#test-your-own-supply) +and [sandbox guide](https://docs.interchange.io/v2/features/sandbox) to establish +the current account's testing path. Retrieve +`get({ kind: "skill", id: "test-sales-agent" })` before a remote test. Follow +that returned version's declared scenarios, fixtures, bounds, and cleanup; +do not copy a scenario from an older installed package. + +## Select the evidence to collect + +- A local protocol or mapping test uses synthetic fixtures and proves only + the application's behavior. +- Brief discovery exercises the authenticated company's own supply and + observes products without staging a buy. It still creates temporary sandbox + resources; it is not the read-only installation check. +- An own-supply transaction stages and activates a sandbox buy after the + scenario's explicit confirmations. Sandbox status must come from the + returned advertiser, not its name or a test-mode string. + +Keep the exact seller, advertiser, campaign, source or agent target, product +query, revision, and offer provenance returned by the tools. A provisional +product page does not authorize staging. Follow the scenario's continuation +steps to a terminal catalog and stop if the selected offer disappears or its +provenance cannot be established. + +When the account cannot retrieve the skill or expose the required tools, +report the missing access or capability and finish any local fixture tests. +Do not route around that boundary with legacy test wrappers or a live buyer +advertiser. + +## Finish with evidence and cleanup + +Run the scenario's declared cleanup in reverse mutation order after success, +failure, or cancellation. Preserve enough IDs to retry cleanup, and report any +resources that remain. Restoring a temporary advertiser activation preference +is part of cleanup when the scenario changes it. + +Report assertions observed, failed, and unexercised; exact server result +references; and cleanup status. A staged record is not delivery, a metadata +creative is not an executed creative handoff, and an observed test is not +independent certification. Never claim a local starter passed without running +its deterministic test command. diff --git a/plugins/apostra-developer/skills/build-with-apostra/references/scenarios.json b/plugins/apostra-developer/skills/build-with-apostra/references/scenarios.json new file mode 100644 index 0000000..95a54aa --- /dev/null +++ b/plugins/apostra-developer/skills/build-with-apostra/references/scenarios.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": "1.0", + "skillId": "build-with-apostra", + "skillVersion": "1.0.0", + "scenarios": [ + { + "id": "install-and-discover", + "title": "Install Apostra and build a seller discovery prototype", + "given": "A TypeScript repository has a package manager and test runner but no Apostra connection. The developer asks for a seller discovery prototype.", + "expect": [ + "The agent inspects the repository and reuses its conventions before scaffolding.", + "It connects MCP directly, marks native plugin installation as pending mirror publication and fresh-install verification, completes host OAuth, and calls get_status before any account mutation.", + "It discovers the current search schema and uses only read-only seller discovery for the first live example.", + "The local fixture test proves successful discovery and an error or continuation case; the agent runs it and reports the observed result.", + "The report separates installation, authentication, live account access, and local test evidence." + ] + }, + { + "id": "instructions-without-connector", + "title": "Complete an instructions-only installation", + "given": "The developer installed the skill with npx skills. No MCP tools are available and the browser OAuth handoff cannot complete in the current session.", + "expect": [ + "The agent identifies that instructions were installed but the connector is absent.", + "It gives the documented remote MCP connection and host login step without requesting a pasted token.", + "It completes and runs the deterministic local example when possible, marking OAuth and live access as incomplete.", + "It neither invents account data nor performs remote mutations to work around missing authentication." + ] + }, + { + "id": "durable-buyer-agent", + "title": "Resume a buyer-agent workflow after restart", + "given": "The developer wants an independently operated buyer agent that compares proposals and waits for an authorized person to launch. The process may restart after a proposal request or launch preview.", + "expect": [ + "The agent chooses a durable application boundary and distinguishes independent agent registration from a user's interactive connection.", + "It retrieves current agent-management documentation and the account catalog instead of inventing a hosted runtime or an unavailable API.", + "The implementation persists request handles, exact offer identities, revisions, and approvals and resumes by reading the saved operation.", + "A deterministic restart test proves no duplicate request, and a changed-revision test proves launch cannot reuse stale approval.", + "The workflow preserves the separate launch preview and confirmation calls." + ] + }, + { + "id": "reporting-pipeline", + "title": "Send campaign reporting to an existing warehouse", + "given": "A Python backend already has a daily scheduler and needs aggregate campaign data in its warehouse. Yesterday's delivery may be corrected, and the destination can fail during a write.", + "expect": [ + "The agent reuses the existing scheduler and selects the documented REST reporting contract for deterministic pulls.", + "It verifies authorized resource access with a read before configuring a remote write and does not reuse an MCP OAuth token for REST.", + "The output preserves currency, time window, unavailable values, and data-source meaning.", + "A deterministic test repeats a window and fails a destination write, proving upsert and checkpoint recovery without lost or double-counted rows.", + "The agent reports freshness from observed evidence and does not claim a scheduled pipeline is delivering before its output is observed." + ] + }, + { + "id": "event-ingestion-handoff", + "title": "Connect a CRM event sender", + "given": "The developer wants CRM conversion events attached to an advertiser. The named CRM has no discovered native connector, and the newly configured source has not received production events.", + "expect": [ + "The agent retrieves the applicable source workflow, searches existing advertiser sources, and does not invent a native connector.", + "It builds and tests a sender using the documented event ingestion contract with stable event IDs and partial-failure handling.", + "It distinguishes source configuration, accepted test traffic, production health, and optional campaign attachment.", + "It does not send production data merely to make setup verification pass." + ] + }, + { + "id": "sales-agent-validation", + "title": "Test an owned sales agent without buying live media", + "given": "A seller asks the developer to test their AdCP agent's discovery and purchase flow, with a particular source as the target.", + "expect": [ + "The agent verifies the account and retrieves its eligible test-sales-agent version before mutations.", + "It uses the least-mutating declared scenario and retains structured sandbox and exact target evidence.", + "It refuses to stage provisional or unattributable products and preserves the scenario's confirmations.", + "It executes reverse cleanup after failure as well as success and reports any remaining resources.", + "The report distinguishes observed assertions from unexercised creative handoff, delivery, and certification." + ] + } + ] +} diff --git a/plugins/buyer-account-setup/.claude-plugin/plugin.json b/plugins/buyer-account-setup/.claude-plugin/plugin.json index 8658bda..23b0cfb 100644 --- a/plugins/buyer-account-setup/.claude-plugin/plugin.json +++ b/plugins/buyer-account-setup/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "buyer-account-setup", "displayName": "Buyer Account Setup", "description": "Prepare an Interchange buyer account for campaign execution.", - "version": "1.0.0+cf3519d98ada", + "version": "1.0.0+babfe6ee31bb", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/buyer-account-setup/.codex-plugin/plugin.json b/plugins/buyer-account-setup/.codex-plugin/plugin.json new file mode 100644 index 0000000..4b128e5 --- /dev/null +++ b/plugins/buyer-account-setup/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "buyer-account-setup", + "version": "1.0.0+babfe6ee31bb", + "description": "Prepare an Interchange buyer account for campaign execution.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "buyer", + "account", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Buyer Account Setup", + "shortDescription": "Prepare an Interchange buyer account for campaign execution.", + "longDescription": "Prepare an Interchange buyer account for campaign execution.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use Buyer Account Setup with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/buyer-campaign-management/.claude-plugin/plugin.json b/plugins/buyer-campaign-management/.claude-plugin/plugin.json index 0958ba9..357615b 100644 --- a/plugins/buyer-campaign-management/.claude-plugin/plugin.json +++ b/plugins/buyer-campaign-management/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "buyer-campaign-management", "displayName": "Buyer Campaign Management", "description": "Discover sellers, create campaigns, request proposals, and manage delivery.", - "version": "1.0.0+06dcf0a1ff64", + "version": "1.0.0+0f4776fce3e1", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/buyer-campaign-management/.codex-plugin/plugin.json b/plugins/buyer-campaign-management/.codex-plugin/plugin.json new file mode 100644 index 0000000..a5949c5 --- /dev/null +++ b/plugins/buyer-campaign-management/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "buyer-campaign-management", + "version": "1.0.0+0f4776fce3e1", + "description": "Discover sellers, create campaigns, request proposals, and manage delivery.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "buyer", + "campaigns", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Buyer Campaign Management", + "shortDescription": "Discover sellers, create campaigns, request proposals, and manage delivery.", + "longDescription": "Discover sellers, create campaigns, request proposals, and manage delivery.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use Buyer Campaign Management with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/buyer-creative-management/.claude-plugin/plugin.json b/plugins/buyer-creative-management/.claude-plugin/plugin.json index f8e719a..55f5247 100644 --- a/plugins/buyer-creative-management/.claude-plugin/plugin.json +++ b/plugins/buyer-creative-management/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "buyer-creative-management", "displayName": "Buyer Creative Management", "description": "Prepare and inspect buyer creative inputs for campaign execution.", - "version": "1.0.0+adf9f32a4568", + "version": "1.0.0+bc5104f8f219", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/buyer-creative-management/.codex-plugin/plugin.json b/plugins/buyer-creative-management/.codex-plugin/plugin.json new file mode 100644 index 0000000..1cacdcf --- /dev/null +++ b/plugins/buyer-creative-management/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "buyer-creative-management", + "version": "1.0.0+bc5104f8f219", + "description": "Prepare and inspect buyer creative inputs for campaign execution.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "buyer", + "creative", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Buyer Creative Management", + "shortDescription": "Prepare and inspect buyer creative inputs for campaign execution.", + "longDescription": "Prepare and inspect buyer creative inputs for campaign execution.", + "developerName": "Scope3 PBC", + "category": "Productivity", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use Buyer Creative Management with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/buyer-reporting/.claude-plugin/plugin.json b/plugins/buyer-reporting/.claude-plugin/plugin.json index 3493637..f95a6e2 100644 --- a/plugins/buyer-reporting/.claude-plugin/plugin.json +++ b/plugins/buyer-reporting/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "buyer-reporting", "displayName": "Buyer Reporting", "description": "Inspect buyer campaign delivery and reporting in Interchange.", - "version": "1.0.0+fb7eddc99041", + "version": "1.0.0+2561f9a64120", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/buyer-reporting/.codex-plugin/plugin.json b/plugins/buyer-reporting/.codex-plugin/plugin.json new file mode 100644 index 0000000..37e925b --- /dev/null +++ b/plugins/buyer-reporting/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "buyer-reporting", + "version": "1.0.0+2561f9a64120", + "description": "Inspect buyer campaign delivery and reporting in Interchange.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "buyer", + "reporting", + "mcp" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Buyer Reporting", + "shortDescription": "Inspect buyer campaign delivery and reporting in Interchange.", + "longDescription": "Inspect buyer campaign delivery and reporting in Interchange.", + "developerName": "Scope3 PBC", + "category": "Monitoring", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use Buyer Reporting with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +} diff --git a/plugins/sales-agent-testing/.claude-plugin/plugin.json b/plugins/sales-agent-testing/.claude-plugin/plugin.json index de420b2..6bd78f3 100644 --- a/plugins/sales-agent-testing/.claude-plugin/plugin.json +++ b/plugins/sales-agent-testing/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "sales-agent-testing", "displayName": "Sales Agent Testing", "description": "Test a first-party or third-party AdCP sales agent with governed buyer workflows.", - "version": "1.0.0+8f94ab418580", + "version": "1.0.0+f8ec2c258c39", "author": { "name": "Scope3 PBC", "url": "https://interchange.io" diff --git a/plugins/sales-agent-testing/.codex-plugin/plugin.json b/plugins/sales-agent-testing/.codex-plugin/plugin.json new file mode 100644 index 0000000..9aec14a --- /dev/null +++ b/plugins/sales-agent-testing/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "sales-agent-testing", + "version": "1.0.0+f8ec2c258c39", + "description": "Test a first-party or third-party AdCP sales agent with governed buyer workflows.", + "author": { + "name": "Scope3 PBC", + "url": "https://interchange.io" + }, + "homepage": "https://docs.interchange.io", + "repository": "https://github.com/scope3data/interchange-plugin", + "license": "Apache-2.0", + "keywords": [ + "advertising", + "adcp", + "sales-agent", + "testing" + ], + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "interface": { + "displayName": "Sales Agent Testing", + "shortDescription": "Test a first-party or third-party AdCP sales agent with governed buyer workflows.", + "longDescription": "Test a first-party or third-party AdCP sales agent with governed buyer workflows.", + "developerName": "Scope3 PBC", + "category": "Testing", + "capabilities": [ + "Apostra MCP", + "Agent skill" + ], + "websiteURL": "https://docs.interchange.io", + "defaultPrompt": [ + "Help me use Sales Agent Testing with Apostra." + ], + "composerIcon": "./assets/composer-icon.png", + "logo": "./assets/directory-icon.png" + } +}