Skip to content

Feature: multipart/form-data support for HTTP protocol plugin #59

@nightscape

Description

@nightscape

Problem

The HTTP protocol plugin currently cannot describe APIs that accept multipart/form-data requests. Setting content_type to "multipart/form-data" today would send the entire body_field as the raw body, which is incorrect — multipart requires boundary-delimited parts, each with their own Content-Disposition and optional Content-Type headers.

multipart/form-data is extremely common in real-world APIs:

  • File uploads (images, documents, videos)
  • Mixed payloads (file + metadata fields in one request)
  • Form submissions with binary data

Current body-handling model

tool arguments
  ├─ URL path params   → substituted into URL template
  ├─ body_field        → single argument becomes raw request body
  ├─ header_fields     → become HTTP headers
  └─ (remaining)       → query parameters

This model assumes a single, flat body. Multipart needs multiple named parts, each potentially with its own content type and encoding.

Proposed Design

Add a new optional field multipart_fields to HttpCallTemplate. When present, the request is encoded as multipart/form-data and the body_field / content_type fields are ignored.

New field

multipart_fields: Optional[Dict[str, MultipartFieldConfig]]

Where MultipartFieldConfig is:

{
  "type": "file | field",
  "content_type": "<mime-type>",
  "filename": "<filename or {arg_name} template>"
}
  • type (required): "file" for binary/file parts, "field" for plain text form fields.
  • content_type (optional): The Content-Type of this part. Defaults to application/octet-stream for file, omitted for field.
  • filename (optional): The filename for the Content-Disposition header. Only meaningful for type: "file". Supports {arg_name} substitution from other tool arguments.

Any tool argument not listed in multipart_fields, header_fields, or matched by URL path params goes to query parameters (same as today).

Argument routing (updated)

tool arguments
  ├─ URL path params     → substituted into URL template
  ├─ multipart_fields    → become multipart form parts  (NEW)
  ├─ header_fields       → become HTTP headers
  └─ (remaining)         → query parameters

multipart_fields and body_field are mutually exclusive. If both are present, that is a validation error.

Examples

Image upload with metadata

{
  "name": "upload_image",
  "call_template_type": "http",
  "url": "https://api.example.com/images/upload",
  "http_method": "POST",
  "multipart_fields": {
    "image": {
      "type": "file",
      "content_type": "image/png",
      "filename": "{original_filename}"
    },
    "description": {
      "type": "field"
    }
  },
  "auth": {
    "auth_type": "api_key",
    "api_key": "Bearer ${API_KEY}",
    "var_name": "Authorization",
    "location": "header"
  }
}

Resulting HTTP request:

POST /images/upload HTTP/1.1
Authorization: Bearer sk-xxx
Content-Type: multipart/form-data; boundary=----utcp-abc123

------utcp-abc123
Content-Disposition: form-data; name="image"; filename="photo.png"
Content-Type: image/png

<binary data decoded from base64>
------utcp-abc123
Content-Disposition: form-data; name="description"

A sunset photo
------utcp-abc123--

Simple file upload (minimal config)

{
  "name": "upload_document",
  "call_template_type": "http",
  "url": "https://api.example.com/documents",
  "http_method": "POST",
  "multipart_fields": {
    "file": { "type": "file" }
  }
}

File data encoding

Tool arguments of type: "file" carry base64-encoded content as a string. The HTTP protocol plugin decodes this before placing it in the multipart part. This is consistent with how JSON-based protocols handle binary data (e.g., OpenAPI's format: "binary" or format: "byte").

OpenAPI conversion

OpenAPI 3.x already describes multipart requests natively:

requestBody:
  content:
    multipart/form-data:
      schema:
        type: object
        properties:
          file:
            type: string
            format: binary
          description:
            type: string
      encoding:
        file:
          contentType: image/png

The OpenApiConverter can map this directly:

  • Properties with format: binary or format: byte"type": "file"
  • Other properties → "type": "field"
  • encoding.<field>.contentType"content_type"

Backwards compatibility

  • Fully backwards compatible: multipart_fields is optional and defaults to None
  • Existing call templates are unaffected
  • No changes to other protocol plugins

Open questions

  1. Multiple files in one field: Should type: "file" support arrays (multiple files under one field name)? Some APIs accept files[] with multiple parts. Could be deferred to a follow-up.
  2. Streaming file content: For very large files, base64 in a JSON argument is impractical. Should there be a way to reference a file path instead? This is a broader concern that affects UTCP's data model beyond just multipart.
  3. Dynamic content type: Should content_type support {arg_name} substitution so the caller can specify the MIME type at call time?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions