diff --git a/SIMPLE_MEDIA_AGENT.md b/SIMPLE_MEDIA_AGENT.md new file mode 100644 index 0000000..5233f2e --- /dev/null +++ b/SIMPLE_MEDIA_AGENT.md @@ -0,0 +1,201 @@ +# Simple Media Agent + +A basic reference implementation of a media agent using MCP (Model Context Protocol). + +## Overview + +This media agent exposes MCP tools that Scope3 platform calls to manage media buying: + +- **get_proposed_tactics**: Fetches products from sales agents and proposes budget allocation based on floor prices +- **manage_tactic**: When assigned, creates media buys by allocating budget to the N cheapest products with overallocation + +**Protocol**: MCP (stdio) - All communication via MCP, no HTTP server needed! + +## Algorithm + +### Budget Allocation +- Fetches all products from registered sales agents +- Sorts products by floor price (cheapest first) +- **Applies overallocation** (default 40%) to ensure delivery targets are met +- Calculates N = number of products where daily budget ≥ min daily budget (default $100) +- Selects N cheapest products +- Divides overallocated budget equally among N products + +**Example**: For a $10,000 campaign with 40% overallocation, the agent allocates $14,000 across media buys. This ensures you hit your $10,000 spend target even with underdelivery. + +## Installation + +```bash +npm install @scope3/agentic-client +``` + +## Usage + +### As MCP Server (Standalone) + +```bash +export SCOPE3_API_KEY=your_api_key +export MIN_DAILY_BUDGET=100 +export OVERALLOCATION_PERCENT=40 +npx simple-media-agent +``` + +The agent runs as an MCP server on stdio. Scope3 platform calls it via MCP protocol. + +### Programmatically + +```typescript +import { SimpleMediaAgent } from '@scope3/agentic-client'; + +const agent = new SimpleMediaAgent({ + scope3ApiKey: process.env.SCOPE3_API_KEY, + scope3BaseUrl: 'https://api.agentic.scope3.com', + minDailyBudget: 100, + overallocationPercent: 40, + name: 'my-media-agent', + version: '1.0.0', +}); + +await agent.start(); +``` + +## Configuration + +### Environment Variables + +- `SCOPE3_API_KEY` (required): Your Scope3 API key +- `SCOPE3_BASE_URL` (optional): Scope3 API base URL (default: https://api.agentic.scope3.com) +- `MIN_DAILY_BUDGET` (optional): Minimum daily budget per product in USD (default: 100) +- `OVERALLOCATION_PERCENT` (optional): Overallocation percentage to ensure delivery (default: 40) + +## MCP Tools + +The agent exposes two MCP tools: + +### `get_proposed_tactics` + +Get tactic proposals based on available products and floor prices. + +**Parameters:** +- `campaignId` (required): Campaign ID +- `seatId` (required): Seat/account ID +- `budgetRange` (optional): Budget range with min/max/currency +- `channels` (optional): Array of media channels +- `countries` (optional): Array of ISO country codes + +**Returns:** +```json +{ + "proposedTactics": [ + { + "tacticId": "simple-passthrough-campaign-123", + "execution": "Passthrough strategy with 40% overallocation...", + "budgetCapacity": 50000, + "pricing": { + "method": "passthrough", + "estimatedCpm": 2.50, + "currency": "USD" + }, + "metadata": { + "productCount": 25, + "avgFloorPrice": 2.50, + "overallocationPercent": 40 + } + } + ] +} +``` + +### `manage_tactic` + +Accept tactic assignment and create media buys. + +**Parameters:** +- `tacticId` (required): Tactic ID from proposal +- `tacticContext` (required): Tactic details including budget +- `brandAgentId` (required): Brand agent ID +- `seatId` (required): Seat/account ID + +**Returns:** +```json +{ + "acknowledged": true, + "mediaBuysCreated": 5, + "allocations": [ + { + "productId": "prod-123", + "budget": 2800, + "cpm": 2.10 + } + ] +} +``` + +## Architecture + +``` +Scope3 Platform → MCP (stdio) → Simple Media Agent → Scope3 API + ↓ + Create Media Buys +``` + +The agent: +1. Receives MCP tool calls from Scope3 platform +2. Fetches products from sales agents via Scope3 API +3. Calculates budget allocation with overallocation +4. Creates media buys via Scope3 API +5. Returns results via MCP response + +## Extending the Agent + +This is a reference implementation. To build your own: + +### Custom Product Selection +```typescript +// Filter products by viewability +const highViewabilityProducts = allProducts.filter(p => + p.metadata?.viewability >= 0.85 +); +``` + +### Custom Budget Allocation +```typescript +// Weight by performance, not just equal division +const budgetPerProduct = allocatedBudget * product.performanceScore / totalScore; +``` + +### Add More Tools +```typescript +this.server.addTool({ + name: 'reallocate_budget', + description: 'Reallocate budget based on performance', + parameters: z.object({ + tacticId: z.string(), + performanceData: z.object({}).passthrough(), + }), + execute: async (args) => { + // Custom reallocation logic + }, +}); +``` + +## Example: Claude Desktop Configuration + +Add to your Claude Desktop MCP config: + +```json +{ + "mcpServers": { + "simple-media-agent": { + "command": "npx", + "args": ["simple-media-agent"], + "env": { + "SCOPE3_API_KEY": "your_key", + "OVERALLOCATION_PERCENT": "40" + } + } + } +} +``` + +Then in Claude: "Use get_proposed_tactics to propose tactics for campaign XYZ with $50,000 budget" diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..4da0d65 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,124 @@ +# Testing Simple Media Agent + +## Prerequisites + +You need: +- Scope3 API key +- Registered sales agents in your Scope3 account +- Products available from those sales agents + +## Method 1: Direct Testing (Recommended First) + +Test the tools directly without MCP to verify logic: + +```bash +export SCOPE3_API_KEY=your_api_key +npx ts-node test-media-agent.ts +``` + +This will: +1. Call `get_proposed_tactics` with a test campaign +2. Call `manage_tactic` to create media buys +3. Show you the results and verify overallocation is working + +## Method 2: Claude Desktop Integration + +Add to your Claude Desktop MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "simple-media-agent": { + "command": "node", + "args": [ + "/path/to/agentic-client/.conductor/zurich-v4/dist/simple-media-agent-server.js" + ], + "env": { + "SCOPE3_API_KEY": "your_api_key_here", + "MIN_DAILY_BUDGET": "100", + "OVERALLOCATION_PERCENT": "40" + } + } + } +} +``` + +Then in Claude Desktop: + +``` +Use get_proposed_tactics to propose tactics for campaign "test-123" with max budget 50000 +``` + +Claude will call your MCP server and show the results! + +## Method 3: MCP Inspector + +Use the MCP Inspector tool to test your server: + +```bash +export SCOPE3_API_KEY=your_key +npx @modelcontextprotocol/inspector node dist/simple-media-agent-server.js +``` + +This opens a web UI where you can: +- See all available tools +- Call tools with test parameters +- View responses in real-time + +## What to Verify + +### get_proposed_tactics +✅ Returns proposed tactics with: +- Correct tactic ID format +- Estimated CPM based on floor prices +- Budget capacity matches request +- No metadata field (spec compliance) + +### manage_tactic +✅ Creates media buys with: +- Sum of all budgets = original budget * 1.4 (40% overallocation) +- Each media buy has correct product ID, sales agent ID, CPM +- Uses MediaBuyProduct structure from ADCP client +- N products where daily budget >= $100 + +## Example Output + +```json +{ + "proposedTactics": [ + { + "tacticId": "simple-passthrough-test-123", + "execution": "Passthrough strategy: distribute budget across 25 products...", + "budgetCapacity": 50000, + "pricing": { + "method": "passthrough", + "estimatedCpm": 2.50, + "currency": "USD" + }, + "sku": "simple-passthrough" + } + ] +} +``` + +## Debugging + +If something fails: + +1. **Check logs**: The server outputs to stderr +2. **Verify API key**: Make sure it's valid and has proper permissions +3. **Check sales agents**: Run `scope3.salesAgents.list()` to verify you have agents registered +4. **Check products**: Run `scope3.products.discover()` to verify products are available + +## Common Issues + +**"No products found"** +- You need to register sales agents first +- Sales agents need to have products available + +**"Budget calculation is wrong"** +- Check that overallocationPercent is set correctly +- Verify: sum of all media buy budgets = totalBudget * (1 + overallocationPercent/100) + +**"Type errors"** +- Make sure you've run `npm run build` after making changes diff --git a/jest.config.js b/jest.config.js index 51956da..4e49cae 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,4 +4,5 @@ module.exports = { roots: ['/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts'], + transformIgnorePatterns: ['node_modules/(?!(fastmcp)/)'], }; diff --git a/media-agent-openapi.yaml b/media-agent-openapi.yaml new file mode 100644 index 0000000..00bb81c --- /dev/null +++ b/media-agent-openapi.yaml @@ -0,0 +1,784 @@ +openapi: 3.0.0 +info: + title: Media Agent Protocol + version: 1.0.0 + description: | + Protocol specification for media agents to implement on their own infrastructure. + + Media agents are autonomous systems that optimize media buying on behalf of advertisers. + When you build a media agent, you implement these endpoints on YOUR servers, and Scope3's + platform will call them to get tactic proposals, assign tactics, and send updates. + + This is similar to how sales agents work - you implement the protocol on your infrastructure, + not in Scope3's codebase. + +servers: + - url: https://media-agent.yourcompany.com + description: Your media agent server (you implement this) + +tags: + - name: Tactic Proposals + description: Endpoints Scope3 calls to get your tactic proposals + - name: Tactic Management + description: Endpoints for managing assigned tactics + - name: Tactic Updates + description: Notifications about changes to tactics you're managing + +paths: + /get-proposed-tactics: + post: + tags: [Tactic Proposals] + summary: Get tactic proposals from your agent + operationId: get_proposed_tactics + description: | + Scope3 calls this endpoint when setting up a campaign to ask what tactics + your agent can handle and how you would approach execution. + + Analyze the campaign and respond with proposed tactics, budget capacity, + and your pricing model. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetProposedTacticsRequest' + responses: + '200': + description: Tactic proposals from your agent + content: + application/json: + schema: + $ref: '#/components/schemas/GetProposedTacticsResponse' + '400': + description: Invalid request + '500': + description: Internal error + + /manage-tactic: + post: + tags: [Tactic Management] + summary: Accept or decline tactic assignment + operationId: manage_tactic + description: | + Scope3 calls this when your agent is assigned to manage a tactic. + You should acknowledge and begin setup, or decline if you can't fulfill it. + + The tactic context contains everything you need: budget, schedule, + targeting constraints, creatives, and any custom fields. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ManageTacticRequest' + responses: + '200': + description: Acknowledgment of tactic assignment + content: + application/json: + schema: + $ref: '#/components/schemas/ManageTacticResponse' + '400': + description: Invalid request + + /tactic-context-updated: + post: + tags: [Tactic Updates] + summary: Notification of tactic changes + operationId: tactic_context_updated + description: | + Scope3 calls this when a tactic is modified by the user or their agent. + Changes may include budget adjustments, schedule changes, or other updates. + + Your agent MUST handle these changes as they may impact targeting, + delivery, or budget allocation. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TacticContextUpdatedRequest' + responses: + '200': + description: Acknowledged + '400': + description: Invalid request + + /tactic-creatives-updated: + post: + tags: [Tactic Updates] + summary: Notification of creative changes + operationId: tactic_creatives_updated + description: | + Scope3 calls this when creatives are added, removed, or modified for a tactic. + + Update your media buys to use the new creative assets. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TacticCreativesUpdatedRequest' + responses: + '200': + description: Acknowledged + '400': + description: Invalid request + + /tactic-feedback: + post: + tags: [Tactic Updates] + summary: Performance feedback from orchestrator + operationId: tactic_feedback + description: | + Scope3 sends performance feedback to help you optimize delivery. + + - deliveryIndex: 100 = on target, <100 = under-delivering, >100 = over-delivering + - performanceIndex: 100 = maximum, relative to target or other tactics + + Your agent MAY use this to adjust targeting, budget allocation, or other settings. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TacticFeedbackRequest' + responses: + '200': + description: Acknowledged + '400': + description: Invalid request + +components: + securitySchemes: + apiKey: + type: apiKey + in: header + name: X-API-Key + description: API key for authenticating Scope3's requests to your server + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT bearer token for OAuth-based authentication + basic: + type: http + scheme: basic + description: Basic authentication with username/password + + schemas: + BudgetRange: + type: object + description: Budget range for campaign planning (buyer typically won't reveal full budget) + properties: + min: + type: number + description: Minimum budget available + example: 50000 + max: + type: number + description: Maximum budget available + example: 150000 + currency: + type: string + description: Currency for budget (ISO 4217 code) + default: USD + example: USD + + TacticPricing: + type: object + required: + - method + - rate + properties: + method: + type: string + enum: [passthrough, revshare, cost_per_unit] + description: How the media agent charges for this tactic (passthrough, revshare, or cost_per_unit) + example: revshare + rate: + type: number + description: Rate for the pricing method (e.g., 0.15 for 15% revshare, 2.50 for $2.50 CPM) + example: 0.15 + currency: + type: string + description: Currency for pricing (ISO 4217 code) + default: USD + example: USD + + CustomField: + type: object + properties: + fieldName: + type: string + description: Name of the custom field + example: targetVCPM + fieldType: + type: string + enum: [string, number, boolean, array, object] + description: Data type of the field + example: number + description: + type: string + description: Help text explaining what this field does + example: Target vCPM in USD + + GetProposedTacticsRequest: + type: object + required: + - campaignId + - seatId + properties: + campaignId: + type: string + description: Campaign ID + example: camp_123 + budgetRange: + $ref: '#/components/schemas/BudgetRange' + startDate: + type: string + format: date-time + description: Campaign start date in UTC (ISO 8601 format) + example: '2025-01-01T00:00:00Z' + endDate: + type: string + format: date-time + description: Campaign end date in UTC (ISO 8601 format) + example: '2025-01-31T23:59:59Z' + channels: + type: array + items: + type: string + enum: [display, video, audio, native, ctv] + description: Advertising channels (aligned with AdCP channel schema) + example: [display, video] + countries: + type: array + items: + type: string + description: ISO 3166-1 alpha-2 country codes + example: [US, CA] + objectives: + type: array + items: + type: string + description: Campaign objectives/outcomes (e.g., awareness, consideration, conversion) + example: [awareness, consideration] + brief: + type: string + description: Campaign brief text + example: Launch campaign for new product... + acceptedPricingMethods: + type: array + items: + type: string + enum: [cpm, vcpm, cpc, cpcv, cpv, cpp, flat_rate] + description: AdCP pricing models acceptable to the buyer for sales agent pricing + example: [cpm, vcpm, flat_rate] + promotedOfferings: + $ref: '#/components/schemas/PromotedOfferings' + seatId: + type: string + description: Seat/account ID for this request + example: seat_456 + + ProposedTactic: + type: object + required: + - tacticId + - execution + - budgetCapacity + - pricing + properties: + tacticId: + type: string + description: Unique identifier for this proposed tactic (you generate this) + example: premium-vcpm-display + execution: + type: string + description: How you would execute this tactic + example: Target premium inventory at $2.50 vCPM with 85% viewability + budgetCapacity: + type: number + description: Maximum budget you can effectively manage + example: 50000 + pricing: + $ref: '#/components/schemas/TacticPricing' + sku: + type: string + description: Identifier for this tactic type + example: premium-vcpm + customFieldsRequired: + type: array + items: + $ref: '#/components/schemas/CustomField' + description: Custom fields needed to execute this tactic + + GetProposedTacticsResponse: + type: object + properties: + proposedTactics: + type: array + items: + $ref: '#/components/schemas/ProposedTactic' + description: List of tactics you can handle (empty array if none) + + TacticContext: + type: object + properties: + budget: + type: number + description: Budget allocated + example: 50000 + budgetCurrency: + type: string + description: Currency for budget (ISO 4217 code) + default: USD + example: USD + startDate: + type: string + format: date-time + description: Tactic start date in UTC (ISO 8601 format) + example: '2025-01-01T00:00:00Z' + endDate: + type: string + format: date-time + description: Tactic end date in UTC (ISO 8601 format) + example: '2025-01-31T23:59:59Z' + channel: + type: string + enum: [display, video, audio, native, ctv] + description: Advertising channel (aligned with AdCP channel schema) + example: display + countries: + type: array + items: + type: string + description: Target countries + example: [US] + creatives: + type: array + items: + $ref: '#/components/schemas/Creative' + description: Creative assets to use (uses Creative from main schema) + brandStandards: + type: array + items: + $ref: '#/components/schemas/BrandStandard' + description: Brand safety and suitability requirements (uses BrandStandard from main schema) + + ManageTacticRequest: + type: object + required: + - tacticId + - tacticContext + - brandAgentId + - seatId + properties: + tacticId: + type: string + description: ID of the tactic (matches one you proposed) + example: premium-vcpm-display + tacticContext: + $ref: '#/components/schemas/TacticContext' + brandAgentId: + type: string + description: Brand agent (advertiser) for this campaign + example: ba_123 + seatId: + type: string + description: Seat/account ID + example: seat_456 + customFields: + type: object + description: Custom fields provided by advertiser + example: { targetVCPM: 2.5 } + + ManageTacticResponse: + type: object + required: + - acknowledged + properties: + acknowledged: + type: boolean + description: true to accept assignment, false to decline + example: true + reason: + type: string + description: Optional reason if declining + example: Insufficient budget for effective optimization + + PatchOperation: + type: object + properties: + op: + type: string + enum: [add, remove, replace] + description: Patch operation type + example: replace + path: + type: string + description: JSON Pointer to changed field + example: /budget + value: + description: New value for the field + + TacticContextUpdatedRequest: + type: object + required: + - tacticId + - tactic + - patch + properties: + tacticId: + type: string + description: Tactic ID + example: premium-vcpm-display + tactic: + type: object + description: Current tactic state (after changes) + patch: + type: array + items: + $ref: '#/components/schemas/PatchOperation' + description: Changes in JSON Patch format (RFC 6902) + + TacticCreativesUpdatedRequest: + type: object + required: + - tacticId + - creatives + - patch + properties: + tacticId: + type: string + description: Tactic ID + example: premium-vcpm-display + creatives: + type: array + items: + $ref: '#/components/schemas/Creative' + description: Updated creative assets + patch: + type: array + items: + $ref: '#/components/schemas/PatchOperation' + description: Changes to creatives array in JSON Patch format + + Creative: + type: object + required: + - creativeId + - name + - status + - createdAt + - updatedAt + properties: + creativeId: + type: string + description: Unique identifier for the creative + example: cr_001 + name: + type: string + description: Name of the creative + example: Summer Campaign Banner + status: + type: string + description: Status of the creative + example: ACTIVE + campaignId: + type: string + description: Campaign this creative belongs to (optional) + example: camp_123 + createdAt: + type: string + format: date-time + description: When the creative was created (ISO 8601 UTC) + example: '2025-01-01T00:00:00Z' + updatedAt: + type: string + format: date-time + description: When the creative was last updated (ISO 8601 UTC) + example: '2025-01-15T14:30:00Z' + + BrandStandard: + type: object + required: + - id + - name + - countryCodes + - channelCodes + - brands + - createdAt + - updatedAt + properties: + id: + type: string + description: Unique identifier for the brand standard + example: bs_001 + name: + type: string + description: Name of the brand standard + example: Premium Brand Safety + description: + type: string + description: Description of the standard (optional) + example: High viewability requirements for premium inventory + countryCodes: + type: array + items: + type: string + description: ISO 3166-1 alpha-2 country codes this standard applies to + example: [US, CA] + channelCodes: + type: array + items: + type: string + description: Channels this standard applies to + example: [display, video] + brands: + type: array + items: + type: string + description: Brand names this standard applies to + example: [Brand A, Brand B] + createdAt: + type: string + format: date-time + description: When the standard was created (ISO 8601 UTC) + example: '2025-01-01T00:00:00Z' + updatedAt: + type: string + format: date-time + description: When the standard was last updated (ISO 8601 UTC) + example: '2025-01-15T14:30:00Z' + + TacticFeedbackRequest: + type: object + required: + - tacticId + - startDate + - endDate + - deliveryIndex + - performanceIndex + properties: + tacticId: + type: string + description: Tactic ID + example: premium-vcpm-display + startDate: + type: string + format: date-time + description: Start of feedback interval in UTC (ISO 8601 format) + example: '2025-01-01T00:00:00Z' + endDate: + type: string + format: date-time + description: End of feedback interval in UTC (ISO 8601 format) + example: '2025-01-07T23:59:59Z' + deliveryIndex: + type: number + description: Delivery performance (100 = on target) + example: 95 + performanceIndex: + type: number + description: Performance vs target or peers (100 = maximum) + example: 110 + + PromotedOfferings: + type: object + description: Complete offering specification combining brand manifest, product selectors, and inline offerings (AdCP spec) + required: + - brand_manifest + properties: + brand_manifest: + oneOf: + - $ref: '#/components/schemas/BrandManifest' + - type: string + description: URL reference to a hosted brand manifest + example: https://brand.example.com/manifest.json + description: Brand information manifest (inline object or URL reference) + product_selectors: + type: object + description: Optional product catalog selectors + properties: + product_ids: + type: array + items: + type: string + description: Specific product IDs to promote + example: [prod_123, prod_456] + offerings: + type: array + description: Inline offerings for campaigns without a product catalog + items: + type: object + required: + - name + properties: + name: + type: string + description: Offering name + example: Winter Sale + description: + type: string + description: Description of what's being offered + example: 20% off all winter products + assets: + type: array + description: Assets specific to this offering + items: + type: object + + BrandManifest: + type: object + description: Brand information manifest containing assets, themes, and guidelines (AdCP spec) + properties: + url: + type: string + format: uri + description: Primary brand URL for context and asset discovery + example: https://brand.example.com + name: + type: string + description: Brand or business name + example: Acme Corporation + logos: + type: array + description: Brand logo assets with semantic tags + items: + type: object + required: + - url + properties: + url: + type: string + format: uri + description: URL to the logo asset + example: https://cdn.example.com/logo.png + tags: + type: array + items: + type: string + description: Semantic tags (e.g., 'dark', 'light', 'square', 'horizontal', 'icon') + example: [dark, horizontal] + width: + type: number + description: Logo width in pixels + example: 200 + height: + type: number + description: Logo height in pixels + example: 50 + colors: + type: object + description: Brand color palette + properties: + primary: + type: string + description: Primary brand color (hex) + example: '#0D9373' + secondary: + type: string + description: Secondary brand color (hex) + example: '#07C983' + accent: + type: string + description: Accent color (hex) + background: + type: string + description: Background color (hex) + text: + type: string + description: Text color (hex) + fonts: + type: object + description: Brand typography guidelines + properties: + primary: + type: string + description: Primary font family name + example: Inter + secondary: + type: string + description: Secondary font family name + example: Georgia + font_urls: + type: array + items: + type: string + format: uri + description: URLs to web font files if using custom fonts + tone: + type: string + description: Brand voice and messaging tone + example: professional + tagline: + type: string + description: Brand tagline or slogan + example: Innovation that moves you forward + product_catalog: + type: object + description: Product catalog information for e-commerce advertisers (enables SKU-level creative generation) + required: + - feed_url + properties: + feed_url: + type: string + format: uri + description: URL to product catalog feed + example: https://brand.example.com/products.xml + feed_format: + type: string + enum: [google_merchant_center, facebook_catalog, custom] + description: Format of the product feed + categories: + type: array + items: + type: string + description: Product categories available in the catalog + example: [electronics, apparel, home_goods] + last_updated: + type: string + format: date-time + description: When the product catalog was last updated + update_frequency: + type: string + enum: [realtime, hourly, daily, weekly] + description: How frequently the product catalog is updated + industry: + type: string + description: Industry or vertical + example: retail + target_audience: + type: string + description: Primary target audience description + example: Tech-savvy millennials aged 25-40 + disclaimers: + type: array + description: Legal disclaimers or required text that must appear in creatives + items: + type: object + required: + - text + properties: + text: + type: string + description: Disclaimer text + context: + type: string + description: When this disclaimer applies (e.g., financial_products, all) + required: + type: boolean + description: Whether this disclaimer must appear + contact: + type: object + description: Brand contact information + properties: + email: + type: string + format: email + description: Contact email + phone: + type: string + description: Contact phone number diff --git a/openapi.yaml b/openapi.yaml index 1c3b72d..b56f3ab 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4669,21 +4669,6 @@ components: example: https://example.com/brand-manifest type: string format: uri - channels: - description: Advertising channels to enable - example: - - app - - display - - ctv - type: array - items: - type: string - enum: - - ctv - - video - - display - - app - - social countryCodes: description: Country codes (ISO 3166-1 alpha-2) example: @@ -4695,17 +4680,6 @@ components: type: string minLength: 2 maxLength: 2 - languages: - description: Language codes (ISO 639-1) - example: - - en - - es - - fr - type: array - items: - type: string - minLength: 2 - maxLength: 2 required: - name UpdateBrandAgentInput: @@ -4732,17 +4706,6 @@ components: example: https://example.com/brand-manifest type: string format: uri - channels: - description: Updated channels - type: array - items: - type: string - enum: - - ctv - - video - - display - - app - - social countryCodes: description: Updated country codes type: array @@ -4750,13 +4713,6 @@ components: type: string minLength: 2 maxLength: 2 - languages: - description: Updated language codes - type: array - items: - type: string - minLength: 2 - maxLength: 2 required: - brandAgentId DeleteBrandAgentInput: @@ -4866,63 +4822,38 @@ components: type: string minLength: 1 maxLength: 255 - description: - description: Story description - type: string prompt: description: Brand story prompt type: string - isPrimary: - description: Whether this is the primary story - type: boolean - channelCodes: - description: Channel codes - type: array - items: - type: string - enum: - - ctv - - video - - display - - app - - social - countryCodes: - description: Country codes - type: array - items: - type: string brands: description: Brand names type: array items: type: string languages: - description: Language codes + description: Language codes (use language_list tool to see available options) type: array items: type: string required: - brandAgentId - name - - isPrimary + - languages UpdateBrandStoryInput: type: object properties: + brandStoryId: + description: Brand story ID + type: string name: description: Story name type: string - previousModelId: - description: Previous model ID (bigint or string) - anyOf: - - type: integer - format: int64 - - type: string prompt: description: Updated brand story prompt type: string minLength: 1 required: - - previousModelId + - brandStoryId - prompt DeleteBrandStoryInput: type: object @@ -5176,11 +5107,6 @@ components: - PAUSED - ARCHIVED - DRAFT - expectedVersion: - description: Expected version for optimistic locking - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 required: - campaignId DeleteCampaignInput: @@ -5192,6 +5118,9 @@ components: example: cmp_987654321 type: string minLength: 1 + hardDelete: + description: 'If true, permanently delete the campaign. Default: false (soft delete/archive)' + type: boolean required: - campaignId GetCampaignSummaryInput: @@ -5306,10 +5235,6 @@ components: campaignId: description: Optional campaign ID (object ID) to assign creative to type: string - version: - type: string - registeredBy: - type: string required: - brandAgentId - name @@ -5320,12 +5245,8 @@ components: type: string name: type: string - description: + status: type: string - assetIds: - type: array - items: - type: string required: - creativeId DeleteCreativeInput: @@ -5500,8 +5421,11 @@ components: properties: mediaBuyId: type: string + confirm: + type: boolean required: - mediaBuyId + - confirm ExecuteMediaBuyInput: type: object properties: @@ -5554,16 +5478,24 @@ components: ListNotificationsInput: type: object properties: - read: + brandAgentId: + type: number + campaignId: + type: string + creativeId: + type: string + tacticId: + type: string + types: + type: array + items: + type: string + unreadOnly: type: boolean - take: - type: integer - exclusiveMinimum: true - maximum: 9007199254740991 - skip: - type: integer - minimum: 0 - maximum: 9007199254740991 + limit: + type: number + offset: + type: number MarkNotificationReadInput: type: object properties: @@ -5776,8 +5708,25 @@ components: type: string name: type: string - organization: + description: + type: string + endpointUrl: type: string + format: uri + protocol: + type: string + enum: + - MCP + - A2A + authenticationType: + type: string + enum: + - API_KEY + - OAUTH + - NO_AUTH + authConfig: + type: object + additionalProperties: {} required: - agentId ListSalesAgentAccountsInput: @@ -5842,6 +5791,35 @@ components: type: array items: type: string + languages: + description: Language codes + type: array + items: + type: string + availableBrandStandards: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string + required: + - id + - name + availableBrandStory: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string + required: + - id + - name required: - campaignId - name @@ -5868,6 +5846,30 @@ components: type: array items: type: string + availableBrandStandards: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string + required: + - id + - name + availableBrandStory: + type: array + items: + type: object + properties: + id: + type: number + name: + type: string + required: + - id + - name required: - tacticId DeleteTacticInput: @@ -5875,8 +5877,11 @@ components: properties: tacticId: type: number + confirm: + type: boolean required: - tacticId + - confirm GetTacticInput: type: object properties: diff --git a/package-lock.json b/package-lock.json index 56a5aa3..ee0f376 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,12 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", - "express": "^4.18.0" + "express": "^4.18.0", + "fastmcp": "^3.20.2", + "zod": "^3.25.76" + }, + "bin": { + "scope3-media-agent": "dist/media-agent-server.js" }, "devDependencies": { "@changesets/changelog-github": "^0.5.1", @@ -556,6 +561,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.13.tgz", @@ -2042,6 +2057,12 @@ "node": ">= 8" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2049,6 +2070,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -2069,6 +2102,36 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3619,7 +3682,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4052,6 +4114,273 @@ "dev": true, "license": "MIT" }, + "node_modules/fastmcp": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-3.20.2.tgz", + "integrity": "sha512-5oQGAMZbxARnChEsfEq8l0O7XrBLCDQaZV0f9QGLXaPn+xjl9xPGJRzMhmuVB++2WTEoC5Ue6AtrvZYDPFSJiw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.2", + "@standard-schema/spec": "^1.0.0", + "execa": "^9.6.0", + "file-type": "^21.0.0", + "fuse.js": "^7.1.0", + "mcp-proxy": "^5.8.1", + "strict-event-emitter-types": "^2.0.0", + "undici": "^7.13.0", + "uri-templates": "^0.2.0", + "xsschema": "0.4.0-beta.5", + "yargs": "^18.0.0", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "fastmcp": "dist/bin/fastmcp.js" + } + }, + "node_modules/fastmcp/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fastmcp/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/fastmcp/node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/fastmcp/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fastmcp/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/fastmcp/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/fastmcp/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4072,6 +4401,27 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4085,6 +4435,24 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4234,6 +4602,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4248,7 +4625,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -4258,7 +4634,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4576,6 +4951,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4746,6 +5141,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4778,6 +5185,18 @@ "node": ">=4" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6126,6 +6545,15 @@ "node": ">= 0.4" } }, + "node_modules/mcp-proxy": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-5.9.0.tgz", + "integrity": "sha512-xonJSkuy4wmwXeykBFl0mjRMt4D0pAGCtFIfBFUz4O80VrO92ruJseIdASJO3wN6MdYHi3vbZLOQol3NsUpg4g==", + "license": "MIT", + "bin": { + "mcp-proxy": "dist/bin/mcp-proxy.js" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6584,6 +7012,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6830,6 +7270,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7574,6 +8029,12 @@ "node": ">= 0.8" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -7659,6 +8120,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7773,6 +8250,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7936,6 +8431,18 @@ "node": ">=0.8.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", @@ -7956,6 +8463,18 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -8015,6 +8534,12 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-templates": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz", + "integrity": "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==", + "license": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8146,11 +8671,44 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xsschema": { + "version": "0.4.0-beta.5", + "resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.4.0-beta.5.tgz", + "integrity": "sha512-73pYwf1hMy++7SnOkghJdgdPaGi+Y5I0SaO6rIlxb1ouV6tEyDbEcXP82kyr32KQVTlUbFj6qewi9eUVEiXm+g==", + "license": "MIT", + "peerDependencies": { + "@valibot/to-json-schema": "^1.0.0", + "arktype": "^2.1.20", + "effect": "^3.16.0", + "sury": "^10.0.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.24.5" + }, + "peerDependenciesMeta": { + "@valibot/to-json-schema": { + "optional": true + }, + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-to-json-schema": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -8218,6 +8776,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 648238f..ebf70b2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "TypeScript client for the Scope3 Agentic API with AdCP webhook support", "main": "dist/index.js", "types": "dist/index.d.ts", + "bin": { + "simple-media-agent": "dist/simple-media-agent-server.js" + }, "scripts": { "build": "npm run type-check && tsc", "dev": "tsc --watch", @@ -12,7 +15,8 @@ "format": "prettier --write \"src/**/*.ts\"", "type-check": "tsc --noEmit", "generate-types": "openapi-typescript openapi.yaml -o src/types/api.ts", - "update-schemas": "curl -f -o openapi.yaml https://docs.agentic.scope3.com/openapi.yaml && npm run generate-types", + "generate-media-agent-types": "openapi-typescript media-agent-openapi.yaml -o src/types/media-agent-api.ts", + "update-schemas": "curl -f -o openapi.yaml https://docs.agentic.scope3.com/openapi.yaml && curl -f -o media-agent-openapi.yaml https://docs.agentic.scope3.com/media-agent-openapi.yaml && npm run generate-types && npm run generate-media-agent-types", "prepare": "husky", "pretest": "npm run type-check", "changeset": "changeset", @@ -30,7 +34,9 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", - "express": "^4.18.0" + "express": "^4.18.0", + "fastmcp": "^3.20.2", + "zod": "^3.25.76" }, "devDependencies": { "@changesets/changelog-github": "^0.5.1", diff --git a/src/client.ts b/src/client.ts index 1b864e1..36ca043 100644 --- a/src/client.ts +++ b/src/client.ts @@ -55,6 +55,9 @@ export class Scope3Client { } await this.mcpClient.close(); + if (this.transport) { + await this.transport.close(); + } this.connected = false; } diff --git a/src/index.ts b/src/index.ts index 6d4e7fe..f52a429 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export { Scope3AgenticClient } from './sdk'; // Legacy export for backwards compatibility export { Scope3AgenticClient as Scope3SDK } from './sdk'; export { WebhookServer } from './webhook-server'; +export { SimpleMediaAgent } from './simple-media-agent'; export type { ClientConfig, ToolResponse, Environment } from './types'; export type { WebhookEvent, WebhookHandler, WebhookServerConfig } from './webhook-server'; diff --git a/src/simple-media-agent-server.ts b/src/simple-media-agent-server.ts new file mode 100644 index 0000000..3c987e2 --- /dev/null +++ b/src/simple-media-agent-server.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env node +import { SimpleMediaAgent } from './simple-media-agent.js'; + +const scope3ApiKey = process.env.SCOPE3_API_KEY; +const scope3BaseUrl = process.env.SCOPE3_BASE_URL; +const minDailyBudget = process.env.MIN_DAILY_BUDGET + ? parseFloat(process.env.MIN_DAILY_BUDGET) + : 100; +const overallocationPercent = process.env.OVERALLOCATION_PERCENT + ? parseFloat(process.env.OVERALLOCATION_PERCENT) + : 40; + +if (!scope3ApiKey) { + console.error('Error: SCOPE3_API_KEY environment variable is required'); + process.exit(1); +} + +const agent = new SimpleMediaAgent({ + scope3ApiKey, + scope3BaseUrl, + minDailyBudget, + overallocationPercent, + name: 'simple-media-agent', + version: '1.0.0', +}); + +agent.start().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); + +console.error(` +Simple Media Agent +- Scope3 Base URL: ${scope3BaseUrl || 'https://api.agentic.scope3.com'} +- Min Daily Budget: $${minDailyBudget} +- Overallocation: ${overallocationPercent}% (sum of all media buy budgets) +- Protocol: MCP (stdio) +`); diff --git a/src/simple-media-agent.ts b/src/simple-media-agent.ts new file mode 100644 index 0000000..4dc6811 --- /dev/null +++ b/src/simple-media-agent.ts @@ -0,0 +1,98 @@ +import { FastMCP } from 'fastmcp'; +import { z } from 'zod'; +import { Scope3AgenticClient } from './sdk'; +import type { SimpleMediaAgentConfig, MediaBuyAllocation } from './simple-media-agent/types'; +import { getProposedTactics } from './simple-media-agent/get-proposed-tactics'; +import { manageTactic } from './simple-media-agent/manage-tactic'; + +/** + * Simple Media Agent that exposes MCP tools + * Called by Scope3 platform via MCP protocol + */ +export class SimpleMediaAgent { + private server: FastMCP; + private scope3: Scope3AgenticClient; + private config: Required< + Omit & { name: string; version: string } + >; + private activeTactics: Map; + + constructor(config: SimpleMediaAgentConfig) { + this.config = { + scope3ApiKey: config.scope3ApiKey, + scope3BaseUrl: config.scope3BaseUrl || 'https://api.agentic.scope3.com', + minDailyBudget: config.minDailyBudget || 100, + overallocationPercent: config.overallocationPercent || 40, + name: config.name || 'simple-media-agent', + version: (config.version as `${number}.${number}.${number}`) || '1.0.0', + }; + + this.server = new FastMCP({ + name: this.config.name, + version: this.config.version as `${number}.${number}.${number}`, + }); + + this.scope3 = new Scope3AgenticClient({ + apiKey: this.config.scope3ApiKey, + baseUrl: this.config.scope3BaseUrl, + }); + + this.activeTactics = new Map(); + this.setupTools(); + } + + private setupTools(): void { + // get_proposed_tactics tool + this.server.addTool({ + name: 'get_proposed_tactics', + description: + 'Get tactic proposals from this media agent. Returns budget allocation based on floor prices from sales agents.', + parameters: z.object({ + campaignId: z.string().describe('Campaign ID'), + budgetRange: z + .object({ + min: z.number(), + max: z.number(), + currency: z.string(), + }) + .optional(), + channels: z.array(z.string()).optional().describe('Media channels'), + countries: z.array(z.string()).optional().describe('ISO country codes'), + seatId: z.string().describe('Seat/account ID'), + }), + execute: async (args) => { + const result = await getProposedTactics(this.scope3, args); + return JSON.stringify(result, null, 2); + }, + }); + + // manage_tactic tool + this.server.addTool({ + name: 'manage_tactic', + description: + 'Assign this media agent to manage a tactic. Creates media buys with overallocated budgets.', + parameters: z.object({ + tacticId: z.string().describe('Tactic ID'), + tacticContext: z.object({}).passthrough().describe('Tactic details including budget'), + brandAgentId: z.string().describe('Brand agent ID'), + seatId: z.string().describe('Seat/account ID'), + }), + execute: async (args) => { + const result = await manageTactic( + this.scope3, + this.config.minDailyBudget, + this.config.overallocationPercent, + this.activeTactics, + args + ); + return JSON.stringify(result, null, 2); + }, + }); + } + + async start(): Promise { + await this.server.start({ + transportType: 'stdio', + }); + } +} diff --git a/src/simple-media-agent/get-proposed-tactics.ts b/src/simple-media-agent/get-proposed-tactics.ts new file mode 100644 index 0000000..055bdad --- /dev/null +++ b/src/simple-media-agent/get-proposed-tactics.ts @@ -0,0 +1,84 @@ +import type { Scope3AgenticClient } from '../sdk'; +import type { ProposedTactic } from './types'; + +export async function getProposedTactics( + scope3: Scope3AgenticClient, + args: { + campaignId: string; + budgetRange?: { min: number; max: number; currency: string }; + seatId: string; + } +): Promise<{ proposedTactics: ProposedTactic[] }> { + const { campaignId, budgetRange } = args; + + // Get all registered sales agents + const salesAgentsResponse = await scope3.salesAgents.list(); + const salesAgents = salesAgentsResponse.data || []; + + if (!Array.isArray(salesAgents)) { + throw new Error('Expected salesAgents to be an array'); + } + + // Call getProducts for each sales agent + const allProducts: Array<{ + id: string; + salesAgentId: string; + floorPrice?: number; + recommendedPrice?: number; + name?: string; + }> = []; + + for (const agent of salesAgents) { + try { + const productsResponse = await scope3.products.discover({ + salesAgentId: agent.id, + }); + + const products = productsResponse.data; + if (!Array.isArray(products)) continue; + + const mappedProducts = products.map((p) => ({ + id: p.id, + salesAgentId: agent.id, + floorPrice: p.floorPrice, + recommendedPrice: p.recommendedPrice, + name: p.name, + })); + + allProducts.push(...mappedProducts); + } catch (error) { + console.error(`Error fetching products from agent ${agent.id}:`, error); + } + } + + // Fail if no products found + if (allProducts.length === 0) { + throw new Error( + `No products available from ${salesAgents.length} sales agents. ` + + 'Cannot propose tactics without available inventory.' + ); + } + + // Sort by floor price (cheapest first) + allProducts.sort((a, b) => (a.floorPrice || 0) - (b.floorPrice || 0)); + + // Calculate average floor price for proposal + const avgFloorPrice = + allProducts.reduce((sum, p) => sum + (p.floorPrice || 0), 0) / allProducts.length; + + return { + proposedTactics: [ + { + tacticId: `simple-passthrough-${campaignId}`, + execution: `Passthrough strategy: distribute budget across ${allProducts.length} products based on floor prices with 40% overallocation.`, + budgetCapacity: budgetRange?.max || 0, + pricing: { + method: 'passthrough', + estimatedCpm: avgFloorPrice, + currency: 'USD', + }, + sku: 'simple-passthrough', + }, + ], + }; +} diff --git a/src/simple-media-agent/manage-tactic.ts b/src/simple-media-agent/manage-tactic.ts new file mode 100644 index 0000000..ab6078a --- /dev/null +++ b/src/simple-media-agent/manage-tactic.ts @@ -0,0 +1,145 @@ +import type { Scope3AgenticClient } from '../sdk'; +import type { MediaBuyAllocation } from './types'; + +function calculateBudgetAllocation( + products: Array<{ + id: string; + salesAgentId: string; + floorPrice?: number; + }>, + totalBudget: number, + minDailyBudget: number, + overallocationPercent: number +): MediaBuyAllocation[] { + if (products.length === 0) return []; + + // Apply overallocation to total budget + // The SUM of all media buy budgets = totalBudget * (1 + overallocationPercent/100) + const overallocationMultiplier = 1 + overallocationPercent / 100; + const allocatedTotalBudget = totalBudget * overallocationMultiplier; + + // Calculate N = number of products where daily budget >= minDailyBudget + const assumedDays = 30; + const maxProducts = Math.floor(allocatedTotalBudget / assumedDays / minDailyBudget); + const n = Math.min(maxProducts, products.length); + + if (n === 0) return []; + + // Take N cheapest products + const selectedProducts = products.slice(0, n); + + // Divide overallocated budget equally + // Each media buy gets: allocatedTotalBudget / N + const budgetPerProduct = allocatedTotalBudget / n; + + return selectedProducts.map((product) => ({ + mediaProductId: product.id, + salesAgentId: product.salesAgentId, + budgetAmount: budgetPerProduct, + budgetCurrency: 'USD', + pricingCpm: product.floorPrice || 0, + })); +} + +export async function manageTactic( + scope3: Scope3AgenticClient, + minDailyBudget: number, + overallocationPercent: number, + tacticAllocations: Map, + args: { + tacticId: string; + tacticContext: { budget?: number }; + brandAgentId: string; + seatId: string; + } +): Promise<{ + acknowledged: boolean; + reason?: string; +}> { + const { tacticId, tacticContext } = args; + + console.log(`Managing tactic ${tacticId}`); + + // Get all registered sales agents + const salesAgentsResponse = await scope3.salesAgents.list(); + const salesAgents = salesAgentsResponse.data || []; + + if (!Array.isArray(salesAgents)) { + throw new Error('Expected salesAgents to be an array'); + } + + // Get products from all sales agents + const allProducts: Array<{ + id: string; + salesAgentId: string; + floorPrice?: number; + recommendedPrice?: number; + name?: string; + }> = []; + + for (const agent of salesAgents) { + try { + const productsResponse = await scope3.products.discover({ + salesAgentId: agent.id, + }); + + const products = productsResponse.data; + if (!Array.isArray(products)) continue; + + const mappedProducts = products.map((p) => ({ + id: p.id, + salesAgentId: agent.id, + floorPrice: p.floorPrice, + recommendedPrice: p.recommendedPrice, + name: p.name, + })); + + allProducts.push(...mappedProducts); + } catch (error) { + console.error(`Error fetching products from agent ${agent.id}:`, error); + } + } + + // Sort by floor price (cheapest first) + allProducts.sort((a, b) => (a.floorPrice || 0) - (b.floorPrice || 0)); + + // Calculate budget allocation with overallocation + const totalBudget = tacticContext.budget || 0; + const allocations = calculateBudgetAllocation( + allProducts, + totalBudget, + minDailyBudget, + overallocationPercent + ); + + // Store tactic info for later reallocation + tacticAllocations.set(tacticId, { + allocations, + }); + + // Create media buys for each allocation + for (const allocation of allocations) { + try { + await scope3.mediaBuys.create({ + tacticId, + name: `Media Buy - ${allocation.mediaProductId}`, + products: [allocation], + budget: { + amount: allocation.budgetAmount || 0, + currency: allocation.budgetCurrency || 'USD', + }, + }); + } catch (error) { + console.error(`Error creating media buy for product ${allocation.mediaProductId}:`, error); + // Return failure if we can't create media buys + return { + acknowledged: false, + reason: `Failed to create media buy for product ${allocation.mediaProductId}`, + }; + } + } + + return { + acknowledged: true, + }; +} diff --git a/src/simple-media-agent/types.ts b/src/simple-media-agent/types.ts new file mode 100644 index 0000000..e6b23ca --- /dev/null +++ b/src/simple-media-agent/types.ts @@ -0,0 +1,25 @@ +import type { MediaBuyProduct } from '../resources/media-buys'; + +export interface SimpleMediaAgentConfig { + scope3ApiKey: string; + scope3BaseUrl?: string; + minDailyBudget?: number; + overallocationPercent?: number; + name?: string; + version?: string; +} + +// MediaBuyAllocation uses MediaBuyProduct from the ADCP client library +export type MediaBuyAllocation = MediaBuyProduct; + +export interface ProposedTactic { + tacticId: string; + execution: string; + budgetCapacity: number; + pricing: { + method: string; + estimatedCpm: number; + currency: string; + }; + sku: string; +} diff --git a/src/types/api.ts b/src/types/api.ts index ff4517f..b7f5113 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -511,15 +511,6 @@ export interface components { * @example https://example.com/brand-manifest */ manifestUrl?: string; - /** - * @description Advertising channels to enable - * @example [ - * "app", - * "display", - * "ctv" - * ] - */ - channels?: ('ctv' | 'video' | 'display' | 'app' | 'social')[]; /** * @description Country codes (ISO 3166-1 alpha-2) * @example [ @@ -529,15 +520,6 @@ export interface components { * ] */ countryCodes?: string[]; - /** - * @description Language codes (ISO 639-1) - * @example [ - * "en", - * "es", - * "fr" - * ] - */ - languages?: string[]; }; /** @description Parameters for updating a brand agent */ UpdateBrandAgentInput: { @@ -556,12 +538,8 @@ export interface components { * @example https://example.com/brand-manifest */ manifestUrl?: string; - /** @description Updated channels */ - channels?: ('ctv' | 'video' | 'display' | 'app' | 'social')[]; /** @description Updated country codes */ countryCodes?: string[]; - /** @description Updated language codes */ - languages?: string[]; }; /** @description Parameters for deleting a brand agent */ DeleteBrandAgentInput: { @@ -614,26 +592,18 @@ export interface components { brandAgentId: number | string; /** @description Story name */ name: string; - /** @description Story description */ - description?: string; /** @description Brand story prompt */ prompt?: string; - /** @description Whether this is the primary story */ - isPrimary: boolean; - /** @description Channel codes */ - channelCodes?: ('ctv' | 'video' | 'display' | 'app' | 'social')[]; - /** @description Country codes */ - countryCodes?: string[]; /** @description Brand names */ brands?: string[]; - /** @description Language codes */ - languages?: string[]; + /** @description Language codes (use language_list tool to see available options) */ + languages: string[]; }; UpdateBrandStoryInput: { + /** @description Brand story ID */ + brandStoryId: string; /** @description Story name */ name?: string; - /** @description Previous model ID (bigint or string) */ - previousModelId: number | string; /** @description Updated brand story prompt */ prompt: string; }; @@ -786,8 +756,6 @@ export interface components { * @enum {string} */ status?: 'ACTIVE' | 'PAUSED' | 'ARCHIVED' | 'DRAFT'; - /** @description Expected version for optimistic locking */ - expectedVersion?: number; }; /** @description Parameters for deleting a campaign */ DeleteCampaignInput: { @@ -796,6 +764,8 @@ export interface components { * @example cmp_987654321 */ campaignId: string; + /** @description If true, permanently delete the campaign. Default: false (soft delete/archive) */ + hardDelete?: boolean; }; GetCampaignSummaryInput: { /** @description Campaign ID */ @@ -849,14 +819,11 @@ export interface components { assemblyMethod?: 'CREATIVE_AGENT' | 'ACTIVATION' | 'PUBLISHER'; /** @description Optional campaign ID (object ID) to assign creative to */ campaignId?: string; - version?: string; - registeredBy?: string; }; UpdateCreativeInput: { creativeId: string; name?: string; - description?: string; - assetIds?: string[]; + status?: string; }; DeleteCreativeInput: { creativeId: string; @@ -923,6 +890,7 @@ export interface components { }; DeleteMediaBuyInput: { mediaBuyId: string; + confirm: boolean; }; ExecuteMediaBuyInput: { mediaBuyId: string; @@ -949,9 +917,14 @@ export interface components { newBudgetAmount: number; }; ListNotificationsInput: { - read?: boolean; - take?: number; - skip?: number; + brandAgentId?: number; + campaignId?: string; + creativeId?: string; + tacticId?: string; + types?: string[]; + unreadOnly?: boolean; + limit?: number; + offset?: number; }; MarkNotificationReadInput: { notificationId: string; @@ -1031,7 +1004,16 @@ export interface components { UpdateSalesAgentInput: { agentId: string; name?: string; - organization?: string; + description?: string; + /** Format: uri */ + endpointUrl?: string; + /** @enum {string} */ + protocol?: 'MCP' | 'A2A'; + /** @enum {string} */ + authenticationType?: 'API_KEY' | 'OAUTH' | 'NO_AUTH'; + authConfig?: { + [key: string]: unknown; + }; }; ListSalesAgentAccountsInput: { agentId: string; @@ -1058,6 +1040,16 @@ export interface components { prompt?: string; channelCodes?: ('ctv' | 'video' | 'display' | 'app' | 'social')[]; countryCodes?: string[]; + /** @description Language codes */ + languages?: string[]; + availableBrandStandards?: { + id: number; + name: string; + }[]; + availableBrandStory?: { + id: number; + name: string; + }[]; }; UpdateTacticInput: { tacticId: number; @@ -1065,9 +1057,18 @@ export interface components { prompt?: string; channelCodes?: ('ctv' | 'video' | 'display' | 'app' | 'social')[]; countryCodes?: string[]; + availableBrandStandards?: { + id: number; + name: string; + }[]; + availableBrandStory?: { + id: number; + name: string; + }[]; }; DeleteTacticInput: { tacticId: number; + confirm: boolean; }; GetTacticInput: { tacticId: number; diff --git a/src/types/media-agent-api.ts b/src/types/media-agent-api.ts new file mode 100644 index 0000000..384a426 --- /dev/null +++ b/src/types/media-agent-api.ts @@ -0,0 +1,771 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/get-proposed-tactics': { + /** + * Get tactic proposals from your agent + * @description Scope3 calls this endpoint when setting up a campaign to ask what tactics + * your agent can handle and how you would approach execution. + * + * Analyze the campaign and respond with proposed tactics, budget capacity, + * and your pricing model. + */ + post: operations['get_proposed_tactics']; + }; + '/manage-tactic': { + /** + * Accept or decline tactic assignment + * @description Scope3 calls this when your agent is assigned to manage a tactic. + * You should acknowledge and begin setup, or decline if you can't fulfill it. + * + * The tactic context contains everything you need: budget, schedule, + * targeting constraints, creatives, and any custom fields. + */ + post: operations['manage_tactic']; + }; + '/tactic-context-updated': { + /** + * Notification of tactic changes + * @description Scope3 calls this when a tactic is modified by the user or their agent. + * Changes may include budget adjustments, schedule changes, or other updates. + * + * Your agent MUST handle these changes as they may impact targeting, + * delivery, or budget allocation. + */ + post: operations['tactic_context_updated']; + }; + '/tactic-creatives-updated': { + /** + * Notification of creative changes + * @description Scope3 calls this when creatives are added, removed, or modified for a tactic. + * + * Update your media buys to use the new creative assets. + */ + post: operations['tactic_creatives_updated']; + }; + '/tactic-feedback': { + /** + * Performance feedback from orchestrator + * @description Scope3 sends performance feedback to help you optimize delivery. + * + * - deliveryIndex: 100 = on target, <100 = under-delivering, >100 = over-delivering + * - performanceIndex: 100 = maximum, relative to target or other tactics + * + * Your agent MAY use this to adjust targeting, budget allocation, or other settings. + */ + post: operations['tactic_feedback']; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + /** @description Budget range for campaign planning (buyer typically won't reveal full budget) */ + BudgetRange: { + /** + * @description Minimum budget available + * @example 50000 + */ + min?: number; + /** + * @description Maximum budget available + * @example 150000 + */ + max?: number; + /** + * @description Currency for budget (ISO 4217 code) + * @default USD + * @example USD + */ + currency?: string; + }; + TacticPricing: { + /** + * @description How the media agent charges for this tactic (passthrough, revshare, or cost_per_unit) + * @example revshare + * @enum {string} + */ + method: 'passthrough' | 'revshare' | 'cost_per_unit'; + /** + * @description Rate for the pricing method (e.g., 0.15 for 15% revshare, 2.50 for $2.50 CPM) + * @example 0.15 + */ + rate: number; + /** + * @description Currency for pricing (ISO 4217 code) + * @default USD + * @example USD + */ + currency?: string; + }; + CustomField: { + /** + * @description Name of the custom field + * @example targetVCPM + */ + fieldName?: string; + /** + * @description Data type of the field + * @example number + * @enum {string} + */ + fieldType?: 'string' | 'number' | 'boolean' | 'array' | 'object'; + /** + * @description Help text explaining what this field does + * @example Target vCPM in USD + */ + description?: string; + }; + GetProposedTacticsRequest: { + /** + * @description Campaign ID + * @example camp_123 + */ + campaignId: string; + budgetRange?: components['schemas']['BudgetRange']; + /** + * Format: date-time + * @description Campaign start date in UTC (ISO 8601 format) + * @example 2025-01-01T00:00:00Z + */ + startDate?: string; + /** + * Format: date-time + * @description Campaign end date in UTC (ISO 8601 format) + * @example 2025-01-31T23:59:59Z + */ + endDate?: string; + /** + * @description Advertising channels (aligned with AdCP channel schema) + * @example [ + * "display", + * "video" + * ] + */ + channels?: ('display' | 'video' | 'audio' | 'native' | 'ctv')[]; + /** + * @description ISO 3166-1 alpha-2 country codes + * @example [ + * "US", + * "CA" + * ] + */ + countries?: string[]; + /** + * @description Campaign objectives/outcomes (e.g., awareness, consideration, conversion) + * @example [ + * "awareness", + * "consideration" + * ] + */ + objectives?: string[]; + /** + * @description Campaign brief text + * @example Launch campaign for new product... + */ + brief?: string; + /** + * @description AdCP pricing models acceptable to the buyer for sales agent pricing + * @example [ + * "cpm", + * "vcpm", + * "flat_rate" + * ] + */ + acceptedPricingMethods?: ('cpm' | 'vcpm' | 'cpc' | 'cpcv' | 'cpv' | 'cpp' | 'flat_rate')[]; + promotedOfferings?: components['schemas']['PromotedOfferings']; + /** + * @description Seat/account ID for this request + * @example seat_456 + */ + seatId: string; + }; + ProposedTactic: { + /** + * @description Unique identifier for this proposed tactic (you generate this) + * @example premium-vcpm-display + */ + tacticId: string; + /** + * @description How you would execute this tactic + * @example Target premium inventory at $2.50 vCPM with 85% viewability + */ + execution: string; + /** + * @description Maximum budget you can effectively manage + * @example 50000 + */ + budgetCapacity: number; + pricing: components['schemas']['TacticPricing']; + /** + * @description Identifier for this tactic type + * @example premium-vcpm + */ + sku?: string; + /** @description Custom fields needed to execute this tactic */ + customFieldsRequired?: components['schemas']['CustomField'][]; + }; + GetProposedTacticsResponse: { + /** @description List of tactics you can handle (empty array if none) */ + proposedTactics?: components['schemas']['ProposedTactic'][]; + }; + TacticContext: { + /** + * @description Budget allocated + * @example 50000 + */ + budget?: number; + /** + * @description Currency for budget (ISO 4217 code) + * @default USD + * @example USD + */ + budgetCurrency?: string; + /** + * Format: date-time + * @description Tactic start date in UTC (ISO 8601 format) + * @example 2025-01-01T00:00:00Z + */ + startDate?: string; + /** + * Format: date-time + * @description Tactic end date in UTC (ISO 8601 format) + * @example 2025-01-31T23:59:59Z + */ + endDate?: string; + /** + * @description Advertising channel (aligned with AdCP channel schema) + * @example display + * @enum {string} + */ + channel?: 'display' | 'video' | 'audio' | 'native' | 'ctv'; + /** + * @description Target countries + * @example [ + * "US" + * ] + */ + countries?: string[]; + /** @description Creative assets to use (uses Creative from main schema) */ + creatives?: components['schemas']['Creative'][]; + /** @description Brand safety and suitability requirements (uses BrandStandard from main schema) */ + brandStandards?: components['schemas']['BrandStandard'][]; + }; + ManageTacticRequest: { + /** + * @description ID of the tactic (matches one you proposed) + * @example premium-vcpm-display + */ + tacticId: string; + tacticContext: components['schemas']['TacticContext']; + /** + * @description Brand agent (advertiser) for this campaign + * @example ba_123 + */ + brandAgentId: string; + /** + * @description Seat/account ID + * @example seat_456 + */ + seatId: string; + /** + * @description Custom fields provided by advertiser + * @example { + * "targetVCPM": 2.5 + * } + */ + customFields?: Record; + }; + ManageTacticResponse: { + /** + * @description true to accept assignment, false to decline + * @example true + */ + acknowledged: boolean; + /** + * @description Optional reason if declining + * @example Insufficient budget for effective optimization + */ + reason?: string; + }; + PatchOperation: { + /** + * @description Patch operation type + * @example replace + * @enum {string} + */ + op?: 'add' | 'remove' | 'replace'; + /** + * @description JSON Pointer to changed field + * @example /budget + */ + path?: string; + /** @description New value for the field */ + value?: unknown; + }; + TacticContextUpdatedRequest: { + /** + * @description Tactic ID + * @example premium-vcpm-display + */ + tacticId: string; + /** @description Current tactic state (after changes) */ + tactic: Record; + /** @description Changes in JSON Patch format (RFC 6902) */ + patch: components['schemas']['PatchOperation'][]; + }; + TacticCreativesUpdatedRequest: { + /** + * @description Tactic ID + * @example premium-vcpm-display + */ + tacticId: string; + /** @description Updated creative assets */ + creatives: components['schemas']['Creative'][]; + /** @description Changes to creatives array in JSON Patch format */ + patch: components['schemas']['PatchOperation'][]; + }; + Creative: { + /** + * @description Unique identifier for the creative + * @example cr_001 + */ + creativeId: string; + /** + * @description Name of the creative + * @example Summer Campaign Banner + */ + name: string; + /** + * @description Status of the creative + * @example ACTIVE + */ + status: string; + /** + * @description Campaign this creative belongs to (optional) + * @example camp_123 + */ + campaignId?: string; + /** + * Format: date-time + * @description When the creative was created (ISO 8601 UTC) + * @example 2025-01-01T00:00:00Z + */ + createdAt: string; + /** + * Format: date-time + * @description When the creative was last updated (ISO 8601 UTC) + * @example 2025-01-15T14:30:00Z + */ + updatedAt: string; + }; + BrandStandard: { + /** + * @description Unique identifier for the brand standard + * @example bs_001 + */ + id: string; + /** + * @description Name of the brand standard + * @example Premium Brand Safety + */ + name: string; + /** + * @description Description of the standard (optional) + * @example High viewability requirements for premium inventory + */ + description?: string; + /** + * @description ISO 3166-1 alpha-2 country codes this standard applies to + * @example [ + * "US", + * "CA" + * ] + */ + countryCodes: string[]; + /** + * @description Channels this standard applies to + * @example [ + * "display", + * "video" + * ] + */ + channelCodes: string[]; + /** + * @description Brand names this standard applies to + * @example [ + * "Brand A", + * "Brand B" + * ] + */ + brands: string[]; + /** + * Format: date-time + * @description When the standard was created (ISO 8601 UTC) + * @example 2025-01-01T00:00:00Z + */ + createdAt: string; + /** + * Format: date-time + * @description When the standard was last updated (ISO 8601 UTC) + * @example 2025-01-15T14:30:00Z + */ + updatedAt: string; + }; + TacticFeedbackRequest: { + /** + * @description Tactic ID + * @example premium-vcpm-display + */ + tacticId: string; + /** + * Format: date-time + * @description Start of feedback interval in UTC (ISO 8601 format) + * @example 2025-01-01T00:00:00Z + */ + startDate: string; + /** + * Format: date-time + * @description End of feedback interval in UTC (ISO 8601 format) + * @example 2025-01-07T23:59:59Z + */ + endDate: string; + /** + * @description Delivery performance (100 = on target) + * @example 95 + */ + deliveryIndex: number; + /** + * @description Performance vs target or peers (100 = maximum) + * @example 110 + */ + performanceIndex: number; + }; + /** @description Complete offering specification combining brand manifest, product selectors, and inline offerings (AdCP spec) */ + PromotedOfferings: { + /** @description Brand information manifest (inline object or URL reference) */ + brand_manifest: components['schemas']['BrandManifest'] | string; + /** @description Optional product catalog selectors */ + product_selectors?: { + /** + * @description Specific product IDs to promote + * @example [ + * "prod_123", + * "prod_456" + * ] + */ + product_ids?: string[]; + }; + /** @description Inline offerings for campaigns without a product catalog */ + offerings?: { + /** + * @description Offering name + * @example Winter Sale + */ + name: string; + /** + * @description Description of what's being offered + * @example 20% off all winter products + */ + description?: string; + /** @description Assets specific to this offering */ + assets?: Record[]; + }[]; + }; + /** @description Brand information manifest containing assets, themes, and guidelines (AdCP spec) */ + BrandManifest: { + /** + * Format: uri + * @description Primary brand URL for context and asset discovery + * @example https://brand.example.com + */ + url?: string; + /** + * @description Brand or business name + * @example Acme Corporation + */ + name?: string; + /** @description Brand logo assets with semantic tags */ + logos?: { + /** + * Format: uri + * @description URL to the logo asset + * @example https://cdn.example.com/logo.png + */ + url: string; + /** + * @description Semantic tags (e.g., 'dark', 'light', 'square', 'horizontal', 'icon') + * @example [ + * "dark", + * "horizontal" + * ] + */ + tags?: string[]; + /** + * @description Logo width in pixels + * @example 200 + */ + width?: number; + /** + * @description Logo height in pixels + * @example 50 + */ + height?: number; + }[]; + /** @description Brand color palette */ + colors?: { + /** + * @description Primary brand color (hex) + * @example #0D9373 + */ + primary?: string; + /** + * @description Secondary brand color (hex) + * @example #07C983 + */ + secondary?: string; + /** @description Accent color (hex) */ + accent?: string; + /** @description Background color (hex) */ + background?: string; + /** @description Text color (hex) */ + text?: string; + }; + /** @description Brand typography guidelines */ + fonts?: { + /** + * @description Primary font family name + * @example Inter + */ + primary?: string; + /** + * @description Secondary font family name + * @example Georgia + */ + secondary?: string; + /** @description URLs to web font files if using custom fonts */ + font_urls?: string[]; + }; + /** + * @description Brand voice and messaging tone + * @example professional + */ + tone?: string; + /** + * @description Brand tagline or slogan + * @example Innovation that moves you forward + */ + tagline?: string; + /** @description Product catalog information for e-commerce advertisers (enables SKU-level creative generation) */ + product_catalog?: { + /** + * Format: uri + * @description URL to product catalog feed + * @example https://brand.example.com/products.xml + */ + feed_url: string; + /** + * @description Format of the product feed + * @enum {string} + */ + feed_format?: 'google_merchant_center' | 'facebook_catalog' | 'custom'; + /** + * @description Product categories available in the catalog + * @example [ + * "electronics", + * "apparel", + * "home_goods" + * ] + */ + categories?: string[]; + /** + * Format: date-time + * @description When the product catalog was last updated + */ + last_updated?: string; + /** + * @description How frequently the product catalog is updated + * @enum {string} + */ + update_frequency?: 'realtime' | 'hourly' | 'daily' | 'weekly'; + }; + /** + * @description Industry or vertical + * @example retail + */ + industry?: string; + /** + * @description Primary target audience description + * @example Tech-savvy millennials aged 25-40 + */ + target_audience?: string; + /** @description Legal disclaimers or required text that must appear in creatives */ + disclaimers?: { + /** @description Disclaimer text */ + text: string; + /** @description When this disclaimer applies (e.g., financial_products, all) */ + context?: string; + /** @description Whether this disclaimer must appear */ + required?: boolean; + }[]; + /** @description Brand contact information */ + contact?: { + /** + * Format: email + * @description Contact email + */ + email?: string; + /** @description Contact phone number */ + phone?: string; + }; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export interface operations { + /** + * Get tactic proposals from your agent + * @description Scope3 calls this endpoint when setting up a campaign to ask what tactics + * your agent can handle and how you would approach execution. + * + * Analyze the campaign and respond with proposed tactics, budget capacity, + * and your pricing model. + */ + get_proposed_tactics: { + requestBody: { + content: { + 'application/json': components['schemas']['GetProposedTacticsRequest']; + }; + }; + responses: { + /** @description Tactic proposals from your agent */ + 200: { + content: { + 'application/json': components['schemas']['GetProposedTacticsResponse']; + }; + }; + /** @description Invalid request */ + 400: { + content: never; + }; + /** @description Internal error */ + 500: { + content: never; + }; + }; + }; + /** + * Accept or decline tactic assignment + * @description Scope3 calls this when your agent is assigned to manage a tactic. + * You should acknowledge and begin setup, or decline if you can't fulfill it. + * + * The tactic context contains everything you need: budget, schedule, + * targeting constraints, creatives, and any custom fields. + */ + manage_tactic: { + requestBody: { + content: { + 'application/json': components['schemas']['ManageTacticRequest']; + }; + }; + responses: { + /** @description Acknowledgment of tactic assignment */ + 200: { + content: { + 'application/json': components['schemas']['ManageTacticResponse']; + }; + }; + /** @description Invalid request */ + 400: { + content: never; + }; + }; + }; + /** + * Notification of tactic changes + * @description Scope3 calls this when a tactic is modified by the user or their agent. + * Changes may include budget adjustments, schedule changes, or other updates. + * + * Your agent MUST handle these changes as they may impact targeting, + * delivery, or budget allocation. + */ + tactic_context_updated: { + requestBody: { + content: { + 'application/json': components['schemas']['TacticContextUpdatedRequest']; + }; + }; + responses: { + /** @description Acknowledged */ + 200: { + content: never; + }; + /** @description Invalid request */ + 400: { + content: never; + }; + }; + }; + /** + * Notification of creative changes + * @description Scope3 calls this when creatives are added, removed, or modified for a tactic. + * + * Update your media buys to use the new creative assets. + */ + tactic_creatives_updated: { + requestBody: { + content: { + 'application/json': components['schemas']['TacticCreativesUpdatedRequest']; + }; + }; + responses: { + /** @description Acknowledged */ + 200: { + content: never; + }; + /** @description Invalid request */ + 400: { + content: never; + }; + }; + }; + /** + * Performance feedback from orchestrator + * @description Scope3 sends performance feedback to help you optimize delivery. + * + * - deliveryIndex: 100 = on target, <100 = under-delivering, >100 = over-delivering + * - performanceIndex: 100 = maximum, relative to target or other tactics + * + * Your agent MAY use this to adjust targeting, budget allocation, or other settings. + */ + tactic_feedback: { + requestBody: { + content: { + 'application/json': components['schemas']['TacticFeedbackRequest']; + }; + }; + responses: { + /** @description Acknowledged */ + 200: { + content: never; + }; + /** @description Invalid request */ + 400: { + content: never; + }; + }; + }; +} diff --git a/test-media-agent.ts b/test-media-agent.ts new file mode 100644 index 0000000..876c8e6 --- /dev/null +++ b/test-media-agent.ts @@ -0,0 +1,105 @@ +#!/usr/bin/env ts-node +/** + * Manual test script for Simple Media Agent + * + * This tests the media agent by calling its tools directly (not via MCP) + * + * Usage: + * SCOPE3_API_KEY=your_key npx ts-node test-media-agent.ts + */ + +import { Scope3AgenticClient } from './src/sdk'; +import { getProposedTactics } from './src/simple-media-agent/get-proposed-tactics'; +import { manageTactic } from './src/simple-media-agent/manage-tactic'; + +async function testMediaAgent() { + const apiKey = process.env.SCOPE3_API_KEY; + + if (!apiKey) { + console.error('Error: SCOPE3_API_KEY environment variable is required'); + process.exit(1); + } + + const scope3 = new Scope3AgenticClient({ + apiKey, + baseUrl: 'https://api.agentic.scope3.com', + }); + + console.log('🧪 Testing Simple Media Agent\n'); + + // Test 1: Get Proposed Tactics + console.log('📋 Test 1: get_proposed_tactics'); + console.log('------------------------------------'); + try { + const proposals = await getProposedTactics(scope3, { + campaignId: 'test-campaign-123', + budgetRange: { + min: 10000, + max: 50000, + currency: 'USD', + }, + seatId: 'test-seat-123', + }); + + console.log('✅ Success!'); + console.log('Proposed Tactics:', JSON.stringify(proposals, null, 2)); + console.log(''); + + // Test 2: Manage Tactic + if (proposals.proposedTactics.length > 0) { + console.log('📋 Test 2: manage_tactic'); + console.log('------------------------------------'); + + const tacticAllocations = new Map(); + + try { + const result = await manageTactic( + scope3, + 100, // minDailyBudget + 40, // overallocationPercent + tacticAllocations, + { + tacticId: proposals.proposedTactics[0].tacticId, + tacticContext: { + budget: 25000, // budget is a number, not an object + }, + brandAgentId: 'test-brand-agent-123', + seatId: 'test-seat-123', + } + ); + + console.log('✅ Success!'); + console.log('Result:', JSON.stringify(result, null, 2)); + + if (result.acknowledged) { + console.log(''); + console.log('📊 Summary:'); + console.log(`- Tactic acknowledged: ${result.acknowledged}`); + console.log(`- Original budget: $25,000`); + console.log(`- Overallocation: 40%`); + console.log(`- Expected total allocated: $35,000`); + } else { + console.log(`- Reason: ${result.reason}`); + } + } catch (error) { + console.error('❌ Error:', error); + } + } + } catch (error) { + console.error('❌ Error:', error); + await scope3.disconnect(); + process.exit(1); + } + + await scope3.disconnect(); +} + +testMediaAgent() + .then(() => { + console.log('\n✅ All tests completed'); + process.exit(0); + }) + .catch((error) => { + console.error('\n❌ Test failed:', error); + process.exit(1); + });