> ## Documentation Index
> Fetch the complete documentation index at: https://tyk.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tyk AI Studio Resource Provider Plugins

> Learn how to register custom resource types that integrate into the App creation flow, participate in privacy scoring, and work with group-based access control.

## Availability

| Edition | Deployment Type |
| :- | :- |
| [Community](https://tyk.io/docs/ai-management/ai-studio/overview.md#community-edition) & [Enterprise](https://tyk.io/docs/ai-management/ai-studio/overview.md#enterprise-edition) | Self-Managed |

```mermaid theme={null}
graph LR
    A[Plugin] -->|Registers| B[Custom Resource Type]
    B --> C[App Creation Flow]
    B --> D[Access Control]
```

Resource Provider plugins allow you to **register custom resource types** that integrate into the App creation flow, participate in privacy scoring, and work with the group-based access control model. This enables plugins to extend the platform's governance model with new kinds of resources beyond the built-in LLMs, Datasources, and Tools.

## Overview

By default, Apps in AI Studio bundle three built-in resource types: LLMs, Datasources, and Tools. The Resource Provider capability lets plugins register additional resource types that:

* **Appear in the Create App form** as selectable resources. In the AI Portal, each type has its own tab in the **Add access** picker. In the admin App form, the type uses a plugin-provided Web Component or a platform-rendered multi-select.
* **Participate in privacy scoring** with the generalized rule: no resource privacy score may exceed the maximum LLM privacy score in the app
* **Integrate with group-based access control** so admins can assign resource instances to groups, and users only see resources available to their groups
* **Support the community submission workflow** so end-users can submit new resource instances for admin review
* **Propagate to gateways** via the config snapshot, so gateway plugins can access resource associations at runtime

### Use Cases

* **MCP Server Registry**: Register MCP servers as a resource type, let users bundle them into Apps
* **Vector Store Catalog**: Expose vector databases with privacy scores for RAG pipelines
* **API Connectors**: Custom API integrations that need governance and access control
* **Knowledge Bases**: Document collections with sensitivity classifications

### How It Works

```text expandable theme={null}
Plugin declares resource types via manifest or GetResourceTypeRegistrations()
    |
Platform registers types in DB, shows them in the App Form
    |
Admin assigns instances to Groups (direct mapping, no catalogues)
    |
User creates App -> selects resources from their accessible instances
    |
Platform validates privacy scores + calls plugin's ValidateResourceSelection()
    |
Associations stored in app_plugin_resources join table
    |
Config snapshot includes associations for gateway access
```

## Implementing a Resource Provider

### 1. Implement the ResourceProvider Interface

```go Expandable expandable theme={null}
import "github.com/TykTechnologies/midsommar/v2/pkg/plugin_sdk"

type MyPlugin struct {
    plugin_sdk.BasePlugin
}

// GetResourceTypeRegistrations declares the resource types this plugin provides.
// Called once after Initialize() and again on plugin reload.
func (p *MyPlugin) GetResourceTypeRegistrations() ([]*plugin_sdk.ResourceTypeRegistration, error) {
    return []*plugin_sdk.ResourceTypeRegistration{
        {
            Slug:            "mcp_servers",
            Name:            "MCP Servers",
            Description:     "Model Context Protocol servers for tool access",
            Icon:            "Hub",
            HasPrivacyScore: true,
            SupportsSubmissions: true,
            // FormComponent: nil means the platform renders a standard multi-select
        },
    }, nil
}

// ListResourceInstances returns all instances for the App form selector.
func (p *MyPlugin) ListResourceInstances(ctx plugin_sdk.Context, slug string) ([]*plugin_sdk.ResourceInstance, error) {
    // Load from plugin KV, external API, or any storage
    servers, err := p.loadServers(ctx)
    if err != nil {
        return nil, err
    }

    var instances []*plugin_sdk.ResourceInstance
    for _, s := range servers {
        instances = append(instances, &plugin_sdk.ResourceInstance{
            ID:           s.ID,
            Name:         s.Name,
            Description:  s.Description,
            PrivacyScore: s.PrivacyScore,
            IsActive:     s.IsActive,
        })
    }
    return instances, nil
}

// GetResourceInstance retrieves a single instance by ID.
func (p *MyPlugin) GetResourceInstance(ctx plugin_sdk.Context, slug, instanceID string) (*plugin_sdk.ResourceInstance, error) {
    server, err := p.loadServer(ctx, instanceID)
    if err != nil {
        return nil, err
    }
    return &plugin_sdk.ResourceInstance{
        ID:           server.ID,
        Name:         server.Name,
        PrivacyScore: server.PrivacyScore,
        IsActive:     server.IsActive,
    }, nil
}

// ValidateResourceSelection is called during app create/update.
// Use this for cross-instance validation (e.g., "max 3 servers per app").
func (p *MyPlugin) ValidateResourceSelection(ctx plugin_sdk.Context, slug string, instanceIDs []string, appID uint32) error {
    if len(instanceIDs) > 5 {
        return fmt.Errorf("maximum 5 MCP servers per app")
    }
    return nil
}

// CreateResourceInstance is called when a community submission is approved.
func (p *MyPlugin) CreateResourceInstance(ctx plugin_sdk.Context, slug string, payload []byte) (*plugin_sdk.ResourceInstance, error) {
    var req CreateServerRequest
    if err := json.Unmarshal(payload, &req); err != nil {
        return nil, err
    }

    server := p.createServer(ctx, req)
    return &plugin_sdk.ResourceInstance{
        ID:   server.ID,
        Name: server.Name,
    }, nil
}
```

### 2. Declare in the Manifest

Add the `resource_types` section to your plugin manifest:

```json Expandable expandable theme={null}
{
  "id": "com.example.mcp-registry",
  "name": "MCP Registry",
  "version": "1.0.0",
  "capabilities": {
    "hooks": ["resource_provider", "studio_ui"]
  },
  "resource_types": [
    {
      "slug": "mcp_servers",
      "name": "MCP Servers",
      "description": "Model Context Protocol servers for tool access",
      "icon": "Hub",
      "has_privacy_score": true,
      "supports_submissions": true,
      "access_granted_via_app": true
    }
  ]
}
```

Resource types declared in the manifest are automatically registered when the plugin loads. The `GetResourceTypeRegistrations()` method provides a runtime fallback and can return additional types not in the manifest.

### Access Class: Does an App Credential Give Access?

From v2.2.0, each type declares if an App credential gives access to its instances. For example, an App credential gives access to an MCP server behind the plugin's gateway proxy. The proxy checks the bindings of the calling App on every request. An App credential does not give access to a cataloged agent or prompt. A developer reads it, or requests access through the plugin's own workflow, and an App binding grants nothing.

`access_granted_via_app` records this difference for each type. Only types where the value is `true`:

* Appear in the App forms (AI Portal builder and admin form)
* Can be bound to an App
* Show the **Build app** action in the AI Portal catalog
* Go to the gateway configuration snapshot (`plugin_resources`)

Types where the value is `false` still appear in the AI Portal catalog, and you can assign them to Teams for visibility. If such a type sets `portal_detail_path`, the catalog card opens the plugin's own page instead of the built-in detail page. Types where the value is `true` can also set `portal_detail_path`. They keep the built-in page and **Build app**, and the built-in page adds a **View in ...** link to the plugin page.

If you leave out the key, AI Studio decides from the plugin hooks. The value is `true` when the plugin declares `resource_provider` and `custom_endpoint`, and `false` if not. Existing plugins need no change. A resource instance can override the value of its type with `ResourceInstance.AccessGrantedViaApp` (`nil` uses the type value).

AI Studio stores the value each time it registers the type (plugin load, or `SyncResourceTypes`). After you upgrade AI Studio, the types of an installed plugin read as `false` until the plugin loads again. This occurs on the first start after the upgrade.

### 3. Serve the Plugin

```go theme={null}
func main() {
    plugin_sdk.Serve(NewMyPlugin())
}
```

## ResourceProvider Interface Reference

| Method | When Called | Purpose |
| - | - | - |
| `GetResourceTypeRegistrations()` | Plugin load/reload | Declare resource types |
| `ListResourceInstances(ctx, slug)` | App form load, Group form load | List available instances |
| `GetResourceInstance(ctx, slug, id)` | Config snapshot build | Get instance details for gateway |
| `ValidateResourceSelection(ctx, slug, ids, appID)` | App create/update | Custom validation logic |
| `CreateResourceInstance(ctx, slug, payload)` | Submission approval | Create instance from approved submission (payload is a `SubmissionEnvelope`) |

## SDK Types

### ResourceTypeRegistration

```go expandable theme={null}
type ResourceTypeRegistration struct {
    Slug                string                 // Machine-readable ID (unique per plugin)
    Name                string                 // Display name in the UI
    Description         string                 // Help text
    Icon                string                 // Material icon name or asset path
    HasPrivacyScore     bool                   // Whether instances carry privacy scores
    SupportsSubmissions bool                   // Whether community submissions are supported
    FormComponent       *ResourceFormComponent // Custom Web Component (nil = standard multi-select)
    SubmissionSchema    string                 // JSON Schema (object) for community submissions
    AccessGrantedViaApp *bool                  // An App credential grants access; nil = platform default
    PortalDetailPath    string                 // Same-origin portal path template, "{id}" replaced
    DefaultAccess       string                 // DefaultAccessAuto (default) or DefaultAccessExplicit (v2.2.1)
}
```

### ResourceInstance

```go theme={null}
type ResourceInstance struct {
    ID                  string // Plugin-assigned unique identifier
    Name                string // Display name
    Description         string // Optional description
    PrivacyScore        int    // 0-100 (only meaningful if type has HasPrivacyScore)
    Metadata            []byte // Opaque JSON included in config snapshots
    IsActive            bool   // Whether instance is currently usable
    AccessGrantedViaApp *bool  // Per-instance override of the type's value; nil inherits
}
```

> **Security**: Do not store secrets, credentials, or PII in the `Metadata` field. Metadata is propagated to all gateways via config snapshots, cached in database join tables, and may appear in logs or be accessible to other plugins with access to the app configuration.

### ResourceFormComponent

```go theme={null}
type ResourceFormComponent struct {
    Tag        string // Web Component custom element tag (e.g., "mcp-server-selector")
    EntryPoint string // JS asset path relative to plugin root (e.g., "ui/webc/selector.js")
}
```

## Privacy Scoring

When `HasPrivacyScore` is `true`, each resource instance carries a privacy score (0-100). The platform enforces a generalized rule during app creation and updates:

> **No resource privacy score may exceed the maximum LLM privacy score in the app.**

This applies to both built-in datasources and plugin resources. For example:

| Resource | Privacy Score | Result |
| - | :-: | - |
| LLM "GPT-4 Enterprise" | 80 | Max LLM score = 80 |
| Datasource "Internal DB" | 60 | OK `(60 <= 80)` |
| Plugin resource "MCP Server A" | 70 | OK `(70 <= 80)` |
| Plugin resource "MCP Server B" | 90 | **Rejected** (90 > 80) |

The plugin sets privacy scores on instances via the `PrivacyScore` field in `ResourceInstance`. Admins review and approve these scores through the submission workflow.

## Access Control

Plugin resource instances use **direct group mapping** instead of the catalogue pattern used by built-in types. This is simpler and sufficient since plugins organize their own resources.

### Access Chain

```
User -> Group -> Plugin Resource Instance (direct)
```

Compare with built-in types:

```
User -> Group -> Catalogue -> LLM/Datasource/Tool
```

### Admin Workflow

1. Admin navigates to **Teams** (Groups) in the admin UI
2. Opens a group and scrolls to the **Plugin Resources** section
3. For each registered resource type, selects which instances this group can access
4. Saves the group

### User Experience

When a user creates an App, they only see resource instances accessible via their group memberships. Admins bypass this filter and see all instances.

### Default Access

By default (`default_access: auto`), AI Studio grants every active instance to the Default Team, so every user sees it. From v2.2.1, AI Studio grants a new `auto` instance immediately. In earlier versions, the instance was not visible until the plugin loaded again.

In the Enterprise Edition, a type can set `default_access: explicit`. Its instances then reach only the Teams that they are granted to. An administrator grants them on the Teams page, or the plugin grants them with `SetResourceInstanceGroups` (scope `resource-access.manage`). Refer to [Team Access to Resource Instances](https://tyk.io/docs/ai-management/ai-studio/plugins/service-api.md#team-access-to-resource-instances-runtime). The Community Edition always uses `auto`.

From v2.2.1, the Teams page lists each plugin resource type and its published instances.

## Custom Form Components

For richer selection UX, plugins can provide a **Web Component** instead of the platform's standard multi-select. Declare it in the `FormComponent` field:

```go theme={null}
FormComponent: &plugin_sdk.ResourceFormComponent{
    Tag:        "mcp-server-selector",
    EntryPoint: "ui/webc/mcp-selector.js",
},
```

The platform loads your Web Component JS from plugin assets and renders it in the App form. The contract:

### Injected Properties

| Property | Type | Description |
| - | - | - |
| `data-selected-ids` | JSON `string[]` | Currently selected instance IDs |
| `data-app-id` | `string` | App ID (empty on create) |
| `data-mode` | `"create"` or `"edit"` | Form mode |
| `pluginAPI.call(method, payload)` | function | Make RPC calls to your plugin backend |
| `pluginAPI.listInstances()` | function | List available instances |

### Events to Dispatch

| Event | Detail | Description |
| - | - | - |
| `selection-change` | `{ selectedIds: string[] }` | Dispatch when user changes selection |

### Example Web Component

```javascript Expandable expandable theme={null}
class MCPServerSelector extends HTMLElement {
  connectedCallback() {
    this.render();

    // Listen for attribute changes from the platform
    const observer = new MutationObserver(() => this.render());
    observer.observe(this, { attributes: true });
  }

  async render() {
    const selectedIds = JSON.parse(this.getAttribute('data-selected-ids') || '[]');
    const instances = await this.pluginAPI.listInstances();

    this.innerHTML = `
      <div class="mcp-selector">
        ${instances.data.map(inst => `
          <label>
            <input type="checkbox" value="${inst.id}"
              ${selectedIds.includes(inst.id) ? 'checked' : ''}>
            ${inst.name} (Privacy: ${inst.privacy_score})
          </label>
        `).join('')}
      </div>
    `;

    this.querySelectorAll('input').forEach(input => {
      input.addEventListener('change', () => {
        const selected = [...this.querySelectorAll('input:checked')]
          .map(el => el.value);
        this.dispatchEvent(new CustomEvent('selection-change', {
          detail: { selectedIds: selected }
        }));
      });
    });
  }
}

customElements.define('mcp-server-selector', MCPServerSelector);
```

## Gateway Integration

Plugin resource associations are included in the config snapshot sent to gateways. Each `AppConfig` includes a `plugin_resources` field:

```protobuf theme={null}
message PluginResourceAssociation {
    uint32 plugin_id = 1;
    string resource_type_slug = 2;
    repeated string instance_ids = 3;
    repeated ResourceInstanceSnapshot instances = 4;
}
```

Gateway plugins can access these associations from the app's config to make routing or authorization decisions. For example, an MCP proxy plugin could check if the requesting app has access to a specific MCP server by examining the `plugin_resources` field.

The snapshot includes only types where `access_granted_via_app` is `true`. Bindings to other types stay on the App record in AI Studio, but AI Studio does not send them to gateways.

## Community Submissions

When `SupportsSubmissions` is `true`, community users can submit new resource instances through the existing submission workflow:

1. The AI Portal submission form lists the type next to Data Source and Tool. If the type declares a `SubmissionSchema`, AI Studio builds the form from the schema. If not, the user enters a name and free-form JSON.
2. The user submits with `resource_type: "plugin"`, `plugin_resource_type_id`, and a `resource_payload`. AI Studio validates the payload against `SubmissionSchema` and refuses invalid payloads with `400`.
3. An administrator reviews the submission in the Submission Queue, and sets the final privacy score.
4. On approval, AI Studio calls the plugin's `CreateResourceInstance(ctx, slug, payload)`. The `payload` is a JSON submission envelope:

```json expandable theme={null}
{
  "source": "submission",
  "submission_id": 123,
  "submitter": {"id": 7, "email": "alice@acme.com", "name": "Alice"},
  "reviewer":  {"id": 1, "email": "admin@acme.com", "name": "Admin"},
  "final_privacy_score": 40,
  "suggested_privacy": 30,
  "privacy_justification": "...",
  "assigned_catalogues": [],
  "documentation_url": "...",
  "notes": "...",
  "attestations": [],
  "resource_payload": { "name": "...", "purpose": "the user's form" }
}
```

Decode it with `plugin_sdk.ParseSubmissionEnvelope(payload)`. This function also accepts a bare payload from older callers.
5\. The plugin creates the instance and returns it. The RPC runs outside the database transaction of AI Studio, so use `submission_id` as an idempotency key. If an instance already exists for the submission, return that instance.
6\. The instance becomes available for selection in the App form.

AI Studio does not support update proposals (`POST /common/submissions/update`) for plugin resource types. Plugins version their own instances.

### Declare a Submission Schema

```go theme={null}
{
    Slug:                "agent",
    Name:                "Agent",
    SupportsSubmissions: true,
    SubmissionSchema:    `{"type":"object","properties":{"name":{"type":"string"},"purpose":{"type":"string"}},"required":["name","purpose"]}`,
}
```

The schema must describe an object. AI Studio validates it when it registers the type, and returns `ErrInvalidSubmissionSchema` if it is not valid.

## Runtime Registration

AI Studio registers the manifest `resource_types` at load time. If the plugin defines its types at runtime (for example, administrators define them inside the plugin), call `plugin_sdk.SyncResourceTypes(ctx, regs)` each time the set changes. The call registers the types in the list and deactivates the plugin's other types. It needs the `resource-types.manage` scope. During the call, AI Studio calls `ListResourceInstances` for each type, so return an empty list (not an error) while your state is still loading.

## Manifest Reference

```json Expandable expandable theme={null}
{
  "resource_types": [
    {
      "slug": "string (required)",
      "name": "string (required)",
      "description": "string",
      "icon": "string",
      "has_privacy_score": false,
      "supports_submissions": false,
      "access_granted_via_app": true,
      "portal_detail_path": "/portal/plugins/my-plugin#/items/{id}",
      "default_access": "auto",
      "form_component": {
        "tag": "string (custom element tag)",
        "entry_point": "string (JS asset path)"
      }
    }
  ]
}
```

| Field | Required | Default | Description |
| - | :-: | :-: | - |
| `slug` | Yes | - | Machine-readable identifier, unique per plugin |
| `name` | Yes | - | Human-readable display name |
| `description` | No | `""` | Description shown in the App form |
| `icon` | No | `""` | Material icon name or plugin asset path |
| `has_privacy_score` | No | `false` | Whether instances carry privacy scores |
| `supports_submissions` | No | `false` | Whether community submissions are enabled |
| `access_granted_via_app` | No | Based on hooks | Whether an App credential gives access to instances. If you leave it out, the value is `true` when the plugin declares `resource_provider` and `custom_endpoint`, and `false` if not. Controls the App forms, the **Build app** action, and the gateway snapshot. |
| `portal_detail_path` | No | `""` | Same-origin path template to the AI Portal page of an instance. AI Studio replaces `{id}` with the escaped instance ID. AI Portal plugin routes match the path exactly, so put the instance ID in the hash of a registered AI Portal route. |
| `default_access` | No | `"auto"` | `auto`: AI Studio grants active instances to the Default Team. `explicit` (Enterprise Edition, v2.2.1 and later): instances reach only the Teams that they are granted to. |
| `form_component` | No | `null` | Custom Web Component for the App form (null = standard multi-select) |

## API Endpoints

These endpoints are available for frontend integration:

| Method | Path | Description |
| - | - | - |
| `GET` | `/api/v1/plugin-resource-types` | List all active registered resource types. Each type includes `access_granted_via_app` and `portal_detail_path`. |
| `GET` | `/common/accessible-plugin-resources` | The types and instances that the caller can use. Each instance includes its effective `access_granted_via_app`. |
| `GET` | `/api/v1/apps/:id/plugin-resources` | Get plugin resources for an app |
| `GET` | `/api/v1/groups/:id/plugin-resources` | Get plugin resource access for a group |
| `PUT` | `/api/v1/groups/:id/plugin-resources` | Set plugin resource access for a group (admin) |

### Set Group Plugin Resources

```bash expandable theme={null}
curl -X PUT /api/v1/groups/5/plugin-resources \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "resources": [
      {
        "plugin_id": 1,
        "resource_type_slug": "mcp_servers",
        "instance_ids": ["server-1", "server-2"]
      }
    ]
  }'
```

## Combining with Other Capabilities

Resource Provider plugins often combine with other capabilities for a complete solution:

| Combination | Purpose |
| - | - |
| ResourceProvider + UIProvider | Admin UI for managing resource instances |
| ResourceProvider + PortalUIProvider | AI Portal self-service for resource browsing |
| ResourceProvider + CustomEndpointHandler | Gateway proxy for resource access (e.g., MCP proxy) |
| ResourceProvider + EdgePayloadReceiver | Aggregate analytics from gateway resource usage |
| ResourceProvider + ConfigProvider | Admin-configurable plugin settings |

### Example: Complete MCP Registry Plugin

```go Expandable expandable theme={null}
type MCPRegistryPlugin struct {
    plugin_sdk.BasePlugin
}

// ResourceProvider - register MCP servers as a resource type
func (p *MCPRegistryPlugin) GetResourceTypeRegistrations() ([]*plugin_sdk.ResourceTypeRegistration, error) { ... }
func (p *MCPRegistryPlugin) ListResourceInstances(ctx plugin_sdk.Context, slug string) ([]*plugin_sdk.ResourceInstance, error) { ... }
func (p *MCPRegistryPlugin) GetResourceInstance(ctx plugin_sdk.Context, slug, id string) (*plugin_sdk.ResourceInstance, error) { ... }
func (p *MCPRegistryPlugin) ValidateResourceSelection(ctx plugin_sdk.Context, slug string, ids []string, appID uint32) error { ... }
func (p *MCPRegistryPlugin) CreateResourceInstance(ctx plugin_sdk.Context, slug string, payload []byte) (*plugin_sdk.ResourceInstance, error) { ... }

// UIProvider - admin UI for server management
func (p *MCPRegistryPlugin) GetAsset(path string) ([]byte, string, error) { ... }
func (p *MCPRegistryPlugin) GetManifest() ([]byte, error) { ... }
func (p *MCPRegistryPlugin) HandleRPC(method string, payload []byte) ([]byte, error) { ... }

// CustomEndpointHandler - gateway proxy for MCP requests
func (p *MCPRegistryPlugin) GetEndpointRegistrations() ([]*pb.EndpointRegistration, error) { ... }
func (p *MCPRegistryPlugin) HandleEndpointRequest(ctx plugin_sdk.Context, req *pb.EndpointRequest) (*pb.EndpointResponse, error) { ... }
```
