import json
from typing import Annotated, Any, Literal
from fastmcp import Context
from fastmcp.server.dependencies import get_http_headers
from pydantic import Field
from signnow_client import SignNowAPIClient
from ..token_provider import TokenProvider
from .create_from_template import _create_from_template
from .document import _get_document, _update_document_fields
from .document_download_link import _get_document_download_link
from .embedded_editor import (
_create_embedded_editor,
_create_embedded_editor_from_template,
)
from .embedded_invite import (
_create_embedded_invite,
_create_embedded_invite_from_template,
)
from .embedded_sending import (
_create_embedded_sending,
_create_embedded_sending_from_template,
)
from .invite_status import _get_invite_status
from .list_documents import _list_document_groups
from .list_templates import _list_all_templates
from .models import (
CreateEmbeddedEditorFromTemplateResponse,
CreateEmbeddedEditorResponse,
CreateEmbeddedInviteFromTemplateResponse,
CreateEmbeddedInviteResponse,
CreateEmbeddedSendingFromTemplateResponse,
CreateEmbeddedSendingResponse,
CreateFromTemplateResponse,
DocumentDownloadLinkResponse,
DocumentGroup,
EmbeddedInviteOrder,
InviteOrder,
InviteStatus,
SendInviteFromTemplateResponse,
SendInviteResponse,
SimplifiedDocumentGroupsResponse,
TemplateSummaryList,
UpdateDocumentFields,
UpdateDocumentFieldsResponse,
)
from .send_invite import _send_invite, _send_invite_from_template
RESOURCE_PREFERRED_SUFFIX = "\n\nPreferred: use this as an MCP Resource (resources/read) when your client supports resources."
TOOL_FALLBACK_SUFFIX = "\n\nNote: If your client supports MCP Resources, prefer the resource version of this endpoint; " "this tool exists as a compatibility fallback for tool-only clients."
def _get_token_and_client(token_provider: TokenProvider) -> tuple[str, SignNowAPIClient]:
"""Get access token and initialize SignNow API client.
Args:
token_provider: TokenProvider instance to get access token
Returns:
Tuple of (access_token, SignNowAPIClient instance)
Raises:
ValueError: If no access token is available
"""
headers = get_http_headers()
token = token_provider.get_access_token(headers)
if not token:
raise ValueError("No access token available")
client = SignNowAPIClient(token_provider.signnow_config)
return token, client
def _normalize_orders(orders: Any, order_type: type) -> list[Any]:
"""Normalize orders parameter - handle both list and JSON string inputs.
Args:
orders: Input orders - can be a list, JSON string, dict, or None
order_type: Type of order model (InviteOrder or EmbeddedInviteOrder)
Returns:
List of order objects (Pydantic models)
"""
if orders is None:
return []
# If it's already a list, validate and convert items if needed
if isinstance(orders, list):
result = []
for item in orders:
if isinstance(item, order_type):
# Already a Pydantic model of the correct type
result.append(item)
elif isinstance(item, dict):
# Convert dict to Pydantic model
result.append(order_type(**item))
else:
# Try to convert other types
result.append(order_type(**item) if hasattr(item, "__dict__") else item)
return result
# If it's a string, try to parse as JSON
if isinstance(orders, str):
try:
parsed = json.loads(orders)
if isinstance(parsed, list):
result = []
for item in parsed:
if isinstance(item, dict):
result.append(order_type(**item))
elif isinstance(item, order_type):
result.append(item)
else:
raise ValueError(f"Invalid order item type in list: {type(item)}")
return result
elif isinstance(parsed, dict):
# Single order object
return [order_type(**parsed)]
else:
raise ValueError(f"Parsed JSON is neither a list nor a dict: {type(parsed)}")
except (json.JSONDecodeError, TypeError, ValueError) as e:
raise ValueError(f"Invalid orders format: {e}") from e
# If it's a dict, wrap in list
if isinstance(orders, dict):
return [order_type(**orders)]
raise ValueError(f"Invalid orders type: {type(orders)}")
def bind(mcp: Any, cfg: Any) -> None:
# Initialize token provider
token_provider = TokenProvider()
async def _list_all_templates_impl(ctx: Context) -> TemplateSummaryList:
token, client = _get_token_and_client(token_provider)
return await _list_all_templates(ctx, token, client)
@mcp.tool(
name="list_all_templates",
description="Get simplified list of all templates and template groups with basic information" + TOOL_FALLBACK_SUFFIX,
tags=["template", "template_group", "list"],
)
async def list_all_templates(ctx: Context) -> TemplateSummaryList:
"""Get all templates and template groups from all folders.
This tool combines both individual templates and template groups into a single response.
Individual templates are marked with entity_type='template' and template groups with entity_type='template_group'.
Note: Individual templates are deprecated. For new implementations, prefer using template groups
which are more feature-rich and actively maintained.
"""
return await _list_all_templates_impl(ctx)
@mcp.resource(
"signnow://templates",
name="list_all_templates_resource",
description="Get simplified list of all templates and template groups with basic information" + RESOURCE_PREFERRED_SUFFIX,
tags=["template", "template_group", "list"],
)
async def list_all_templates_resource(ctx: Context) -> TemplateSummaryList:
return await _list_all_templates_impl(ctx)
def _list_document_groups_impl(ctx: Context, limit: int = 50, offset: int = 0) -> SimplifiedDocumentGroupsResponse:
token, client = _get_token_and_client(token_provider)
return _list_document_groups(token, client, limit, offset)
@mcp.tool(name="list_document_groups", description="Get simplified list of document groups with basic information." + TOOL_FALLBACK_SUFFIX, tags=["document_group", "list"])
def list_document_groups(
ctx: Context,
limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of document groups to return (default: 50, max: 50)")] = 50,
offset: Annotated[int, Field(ge=0, description="Number of document groups to skip for pagination (default: 0)")] = 0,
) -> SimplifiedDocumentGroupsResponse:
"""Provide simplified list of document groups with basic fields.
Args:
limit: Maximum number of document groups to return (default: 50, max: 50)
offset: Number of document groups to skip for pagination (default: 0)
"""
return _list_document_groups_impl(ctx, limit, offset)
@mcp.resource(
"signnow://document-groups/{?limit,offset}",
name="list_document_groups_resource",
description="Get simplified list of document groups with basic information." + RESOURCE_PREFERRED_SUFFIX,
tags=["document_group", "list"],
mime_type="application/json",
)
def list_document_groups_resource(
ctx: Context,
limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of document groups to return (default: 50, max: 50)")] = 50,
offset: Annotated[int, Field(ge=0, description="Number of document groups to skip for pagination(default: 0)")] = 0,
) -> SimplifiedDocumentGroupsResponse:
return _list_document_groups_impl(ctx, limit, offset)
@mcp.tool(
name="send_invite",
description=(
"Send invite to sign a document or document group. "
"This tool is ONLY for documents and document groups. "
"If you have template or template_group, use the alternative tool: send_invite_from_template"
),
tags=["send_invite", "document", "document_group", "sign", "workflow"],
)
def send_invite(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
orders: Annotated[
list[InviteOrder] | str | None,
Field(
description="List of orders with recipients (can be a list or JSON string)",
examples=[
[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign"}]}],
'[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign"}]}]',
],
),
] = None,
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> SendInviteResponse:
"""Send invite to sign a document or document group.
This tool is ONLY for documents and document groups.
If you have template or template_group, use the alternative tool: send_invite_from_template
Args:
entity_id: ID of the document or document group
orders: List of orders with recipients (can be a list or JSON string)
entity_type: Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
Returns:
SendInviteResponse with invite ID and entity type
"""
token, client = _get_token_and_client(token_provider)
# Normalize orders parameter (handle JSON string input)
normalized_orders = _normalize_orders(orders, InviteOrder)
# Initialize client and use the imported function from send_invite module
return _send_invite(entity_id, entity_type, normalized_orders, token, client)
@mcp.tool(
name="create_embedded_invite",
description=(
"Create embedded invite for signing a document or document group. "
"This tool is ONLY for documents and document groups. "
"If you have template or template_group, use the alternative tool: create_embedded_invite_from_template"
),
tags=["send_invite", "document", "document_group", "sign", "embedded", "workflow"],
)
def create_embedded_invite(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
orders: Annotated[
list[EmbeddedInviteOrder] | str | None,
Field(
description="List of orders with recipients (can be a list or JSON string)",
examples=[
[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign", "auth_method": "none"}]}],
'[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign", "auth_method": "none"}]}]',
],
),
] = None,
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> CreateEmbeddedInviteResponse:
"""Create embedded invite for signing a document or document group.
This tool is ONLY for documents and document groups.
If you have template or template_group, use the alternative tool: create_embedded_invite_from_template
Args:
entity_id: ID of the document or document group
orders: List of orders with recipients (can be a list or JSON string)
entity_type: Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
Returns:
CreateEmbeddedInviteResponse with invite ID and entity type
"""
token, client = _get_token_and_client(token_provider)
# Normalize orders parameter (handle JSON string input)
normalized_orders = _normalize_orders(orders, EmbeddedInviteOrder)
# Initialize client and use the imported function from embedded_invite module
return _create_embedded_invite(entity_id, entity_type, normalized_orders, token, client)
@mcp.tool(
name="create_embedded_sending",
description=(
"Create embedded sending for managing, editing, or sending invites for a document or document group. "
"This tool is ONLY for documents and document groups. "
"If you have template or template_group, use the alternative tool: create_embedded_sending_from_template"
),
tags=["edit", "document", "document_group", "send_invite", "embedded", "workflow"],
)
def create_embedded_sending(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
redirect_uri: Annotated[str | None, Field(description="Optional redirect URI after completion")] = None,
redirect_target: Annotated[str | None, Field(description="Optional redirect target: 'self' (default), 'blank'")] = None,
link_expiration: Annotated[int | None, Field(ge=14, le=45, description="Optional link expiration in days (14-45)")] = None,
type: Annotated[Literal["manage", "edit", "send-invite"] | None, Field(description="Type of sending step: 'manage', 'edit', or 'send-invite'")] = "manage",
) -> CreateEmbeddedSendingResponse:
"""Create embedded sending for managing, editing, or sending invites for a document or document group.
This tool is ONLY for documents and document groups.
If you have template or template_group, use the alternative tool: create_embedded_sending_from_template
Args:
entity_id: ID of the document or document group
entity_type: Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
redirect_uri: Optional redirect URI for the sending link
redirect_target: Optional redirect target for the sending link
link_expiration: Optional number of days for the sending link to expire (14-45)
type: Specifies the sending step: 'manage' (default), 'edit', 'send-invite'
Returns:
CreateEmbeddedSendingResponse with entity type, and URL
"""
token, client = _get_token_and_client(token_provider)
return _create_embedded_sending(entity_id, entity_type, redirect_uri, redirect_target, link_expiration, type, token, client)
@mcp.tool(
name="create_embedded_editor",
description=(
"Create embedded editor for editing a document or document group. "
"This tool is ONLY for documents and document groups. "
"If you have template or template_group, use the alternative tool: create_embedded_editor_from_template"
),
tags=["edit", "document", "document_group", "embedded"],
)
def create_embedded_editor(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
redirect_uri: Annotated[str | None, Field(description="Optional redirect URI after completion")] = None,
redirect_target: Annotated[str | None, Field(description="Optional redirect target: 'self' (default), 'blank'")] = None,
link_expiration: Annotated[int | None, Field(ge=15, le=43200, description="Optional link expiration in minutes (15-43200)")] = None,
) -> CreateEmbeddedEditorResponse:
"""Create embedded editor for editing a document or document group.
This tool is ONLY for documents and document groups.
If you have template or template_group, use the alternative tool: create_embedded_editor_from_template
Args:
entity_id: ID of the document or document group
entity_type: Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
redirect_uri: Optional redirect URI for the editor link
redirect_target: Optional redirect target for the editor link
link_expiration: Optional number of minutes for the editor link to expire (15-43200)
Returns:
CreateEmbeddedEditorResponse with editor ID and entity type
"""
token, client = _get_token_and_client(token_provider)
return _create_embedded_editor(entity_id, entity_type, redirect_uri, redirect_target, link_expiration, token, client)
@mcp.tool(
name="create_from_template",
description="Create a new document or document group from an existing template or template group",
tags=["template", "template_group", "document", "document_group", "create", "workflow"],
)
def create_from_template(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the template or template group")],
entity_type: Annotated[
Literal["template", "template_group"] | None,
Field(description="Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
name: Annotated[str | None, Field(description="Optional name for the new document group or document (required for template groups)")] = None,
) -> CreateFromTemplateResponse:
"""Create a new document or document group from an existing template or template group.
Args:
entity_id: ID of the template or template group
entity_type: Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
name: Optional name for the new document group or document (required for template groups)
Returns:
CreateFromTemplateResponse with created entity ID, type and name
"""
token, client = _get_token_and_client(token_provider)
return _create_from_template(entity_id, entity_type, name, token, client)
@mcp.tool(
name="send_invite_from_template",
description=(
"Create document/group from template and send invite immediately. "
"This tool is ONLY for templates and template groups. "
"If you have document or document_group, use the alternative tool: send_invite"
),
tags=["template", "template_group", "document", "document_group", "send_invite", "workflow"],
)
async def send_invite_from_template(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the template or template group")],
orders: Annotated[
list[InviteOrder] | str,
Field(
description="List of orders with recipients for the invite (can be a list or JSON string)",
examples=[
[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign"}]}],
'[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign"}]}]',
],
),
],
entity_type: Annotated[
Literal["template", "template_group"] | None,
Field(description="Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
name: Annotated[str | None, Field(description="Optional name for the new document or document group")] = None,
) -> SendInviteFromTemplateResponse:
"""Create document or document group from template and send invite immediately.
This tool is ONLY for templates and template groups.
If you have document or document_group, use the alternative tool: send_invite
This tool combines two operations:
1. Creates a document/group from template using create_from_template
2. Sends an invite to the created entity using send_invite
Args:
entity_id: ID of the template or template group
orders: List of orders with recipients for the invite (can be a list or JSON string)
entity_type: Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
name: Optional name for the new document or document group
Returns:
SendInviteFromTemplateResponse with created entity info and invite details
"""
token, client = _get_token_and_client(token_provider)
# Normalize orders parameter (handle JSON string input)
normalized_orders = _normalize_orders(orders, InviteOrder)
# Initialize client and use the imported function from send_invite module
return await _send_invite_from_template(entity_id, entity_type, name, normalized_orders, token, client, ctx)
@mcp.tool(
name="create_embedded_sending_from_template",
description=(
"Create document/group from template and create embedded sending immediately. "
"This tool is ONLY for templates and template groups. "
"If you have document or document_group, use the alternative tool: create_embedded_sending"
),
tags=["template", "template_group", "document", "document_group", "send_invite", "embedded", "workflow"],
)
async def create_embedded_sending_from_template(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the template or template group")],
entity_type: Annotated[
Literal["template", "template_group"] | None,
Field(description="Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
name: Annotated[str | None, Field(description="Optional name for the new document or document group")] = None,
redirect_uri: Annotated[str | None, Field(description="Optional redirect URI after completion")] = None,
redirect_target: Annotated[str | None, Field(description="Optional redirect target: 'self' (default), 'blank'")] = None,
link_expiration: Annotated[int | None, Field(ge=14, le=45, description="Optional link expiration in days (14-45)")] = None,
type: Annotated[Literal["manage", "edit", "send-invite"] | None, Field(description="Type of sending step: 'manage', 'edit', or 'send-invite'")] = None,
) -> CreateEmbeddedSendingFromTemplateResponse:
"""Create document or document group from template and create embedded sending immediately.
This tool is ONLY for templates and template groups.
If you have document or document_group, use the alternative tool: create_embedded_sending
This tool combines two operations:
1. Creates a document/group from template using create_from_template
2. Creates an embedded sending for the created entity using create_embedded_sending
Args:
entity_id: ID of the template or template group
entity_type: Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
name: Optional name for the new document or document group
redirect_uri: Optional redirect URI after completion
redirect_target: Optional redirect target: 'self', 'blank', or 'self' (default)
link_expiration: Optional link expiration in days (14-45)
type: Type of sending step: 'manage', 'edit', or 'send-invite'
Returns:
CreateEmbeddedSendingFromTemplateResponse with created entity info and embedded sending details
"""
token, client = _get_token_and_client(token_provider)
return await _create_embedded_sending_from_template(entity_id, entity_type, name, redirect_uri, redirect_target, link_expiration, type, token, client, ctx)
@mcp.tool(
name="create_embedded_editor_from_template",
description=(
"Create document/group from template and create embedded editor immediately. "
"This tool is ONLY for templates and template groups. "
"If you have document or document_group, use the alternative tool: create_embedded_editor"
),
tags=["template", "template_group", "document", "document_group", "embedded_editor", "embedded", "workflow"],
)
async def create_embedded_editor_from_template(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the template or template group")],
entity_type: Annotated[
Literal["template", "template_group"] | None,
Field(description="Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
name: Annotated[str | None, Field(description="Name for the new document or document group")] = None,
redirect_uri: Annotated[str | None, Field(description="Optional redirect URI after completion")] = None,
redirect_target: Annotated[str | None, Field(description="Optional redirect target: 'self' (default), 'blank'")] = None,
link_expiration: Annotated[int | None, Field(ge=15, le=43200, description="Optional link expiration in minutes (15-43200)")] = None,
) -> CreateEmbeddedEditorFromTemplateResponse:
"""Create document or document group from template and create embedded editor immediately.
This tool is ONLY for templates and template groups.
If you have document or document_group, use the alternative tool: create_embedded_editor
This tool combines two operations:
1. Creates a document/group from template using create_from_template
2. Creates an embedded editor for the created entity using create_embedded_editor
Args:
entity_id: ID of the template or template group
entity_type: Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
name: Optional name for the new document or document group
redirect_uri: Optional redirect URI after completion
redirect_target: Optional redirect target: 'self', 'blank', or 'self' (default)
link_expiration: Optional link expiration in minutes (15-43200)
Returns:
CreateEmbeddedEditorFromTemplateResponse with created entity info and embedded editor details
"""
token, client = _get_token_and_client(token_provider)
# Initialize client and use the imported function from embedded_editor module
return await _create_embedded_editor_from_template(entity_id, entity_type, name, redirect_uri, redirect_target, link_expiration, token, client, ctx)
@mcp.tool(
name="create_embedded_invite_from_template",
description=(
"Create document/group from template and create embedded invite immediately. "
"This tool is ONLY for templates and template groups. "
"If you have document or document_group, use the alternative tool: create_embedded_invite"
),
tags=["template", "template_group", "document", "document_group", "send_invite", "embedded", "workflow"],
)
async def create_embedded_invite_from_template(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the template or template group")],
orders: Annotated[
list[EmbeddedInviteOrder] | str | None,
Field(
description="List of orders with recipients for the embedded invite (can be a list or JSON string)",
examples=[
[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign", "auth_method": "none"}]}],
'[{"order": 1, "recipients": [{"email": "user@example.com", "role": "Signer 1", "action": "sign", "auth_method": "none"}]}]',
],
),
] = None,
entity_type: Annotated[
Literal["template", "template_group"] | None,
Field(description="Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
name: Annotated[str | None, Field(description="Optional name for the new document or document group")] = None,
) -> CreateEmbeddedInviteFromTemplateResponse:
"""Create document or document group from template and create embedded invite immediately.
This tool is ONLY for templates and template groups.
If you have document or document_group, use the alternative tool: create_embedded_invite
This tool combines two operations:
1. Creates a document/group from template using create_from_template
2. Creates an embedded invite for the created entity using create_embedded_invite
Args:
entity_id: ID of the template or template group
orders: List of orders with recipients for the embedded invite (can be a list or JSON string)
entity_type: Type of entity: 'template' or 'template_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
name: Optional name for the new document or document group
Returns:
CreateEmbeddedInviteFromTemplateResponse with created entity info and embedded invite details
"""
token, client = _get_token_and_client(token_provider)
# Normalize orders parameter (handle JSON string input)
normalized_orders = _normalize_orders(orders, EmbeddedInviteOrder)
# Initialize client and use the imported function from embedded_invite module
return await _create_embedded_invite_from_template(entity_id, entity_type, name, normalized_orders, token, client, ctx)
def _get_invite_status_impl(ctx: Context, entity_id: str, entity_type: Literal["document", "document_group"] | None) -> InviteStatus:
token, client = _get_token_and_client(token_provider)
return _get_invite_status(entity_id, entity_type, token, client)
@mcp.tool(name="get_invite_status", description="Get invite status for a document or document group" + TOOL_FALLBACK_SUFFIX, tags=["invite", "status", "document", "document_group", "workflow"])
def get_invite_status(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> InviteStatus:
return _get_invite_status_impl(ctx, entity_id, entity_type)
@mcp.resource(
"signnow://invite-status/{entity_id}{?entity_type}",
name="get_invite_status_resource",
description="Get invite status for a document or document group" + RESOURCE_PREFERRED_SUFFIX,
tags=["invite", "status", "document", "document_group", "workflow"],
)
def get_invite_status_resource(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> InviteStatus:
return _get_invite_status_impl(ctx, entity_id, entity_type)
def _get_document_download_link_impl(ctx: Context, entity_id: str, entity_type: Literal["document", "document_group"] | None) -> DocumentDownloadLinkResponse:
token, client = _get_token_and_client(token_provider)
# Initialize client and use the imported function from document_download_link module
return _get_document_download_link(entity_id, entity_type, token, client)
@mcp.tool(name="get_document_download_link", description="Get download link for a document or document group" + TOOL_FALLBACK_SUFFIX, tags=["document", "document_group", "download", "link"])
def get_document_download_link(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> DocumentDownloadLinkResponse:
"""Get download link for a document or document group.
For documents: Returns direct download link.
For document groups: Merges all documents in the group and returns download link for the merged document.
Args:
entity_id: ID of the document or document group
entity_type: Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type.
Returns:
DocumentDownloadLinkResponse with download link
"""
return _get_document_download_link_impl(ctx, entity_id, entity_type)
@mcp.resource(
"signnow://document-download-link/{entity_id}{?entity_type}",
name="get_document_download_link_resource",
description="Get download link for a document or document group" + RESOURCE_PREFERRED_SUFFIX,
tags=["document", "document_group", "download", "link"],
)
def get_document_download_link_resource(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document or document group")],
entity_type: Annotated[
Literal["document", "document_group"] | None,
Field(description="Type of entity: 'document' or 'document_group' (optional). If you're passing it, make sure you know what type you have. If it's not found, try using a different type."),
] = None,
) -> DocumentDownloadLinkResponse:
return _get_document_download_link_impl(ctx, entity_id, entity_type)
def _get_document_impl(ctx: Context, entity_id: str, entity_type: Literal["document", "document_group", "template", "template_group"] | None) -> DocumentGroup:
token, client = _get_token_and_client(token_provider)
# Initialize client and use the imported function from document module
return _get_document(client, token, entity_id, entity_type)
@mcp.tool(
name="get_document",
description="Get full document, template, template group or document group information with field values",
tags=["document", "document_group", "template", "template_group", "get", "fields"],
)
def get_document(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document, template, template group or document group to retrieve")],
entity_type: Annotated[
Literal["document", "document_group", "template", "template_group"] | None,
Field(description="Type of entity: 'document', 'template', 'template_group' or 'document_group' (optional). If not provided, will be determined automatically"),
] = None,
) -> DocumentGroup:
"""Get full document, template, template group or document group information with field values.
Always returns a unified DocumentGroup wrapper even for a single document.
This tool retrieves complete information about a document, template, template group or document group,
including all field values, roles, and metadata. If entity_type is not provided,
the tool will automatically determine whether the entity is a document, template, template group or document group.
For documents, returns a DocumentGroup with a single document.
For templates, returns a DocumentGroup with a single template.
For template groups, returns a DocumentGroup with all templates in the group.
For document groups, returns a DocumentGroup with all documents in the group.
Args:
entity_id: ID of the document, template, template group or document group to retrieve
entity_type: Type of entity: 'document', 'template', 'template_group' or 'document_group' (optional)
Returns:
DocumentGroup with complete information including field values for all documents
"""
return _get_document_impl(ctx, entity_id, entity_type)
@mcp.resource(
"signnow://document/{entity_id}{?entity_type}",
name="get_document_resource",
description="Get full document, template, template group or document group information with field values" + RESOURCE_PREFERRED_SUFFIX,
tags=["document", "document_group", "template", "template_group", "get", "fields"],
)
def get_document_resource(
ctx: Context,
entity_id: Annotated[str, Field(description="ID of the document, template, template group or document group to retrieve")],
entity_type: Annotated[
Literal["document", "document_group", "template", "template_group"] | None,
Field(description="Type of entity: 'document', 'template', 'template_group' or 'document_group' (optional). If not provided, will be determined automatically"),
] = None,
) -> DocumentGroup:
return _get_document_impl(ctx, entity_id, entity_type)
@mcp.tool(
name="update_document_fields",
description="Update text fields in multiple documents (only individual documents, not document groups)",
tags=["document", "fields", "update", "prefill"],
)
def update_document_fields(
ctx: Context,
update_requests: Annotated[
list[UpdateDocumentFields],
Field(
description="Array of document field update requests",
examples=[
[
{
"document_id": "abc123",
"fields": [
{"name": "FieldName1", "value": "New Value 1"},
{"name": "FieldName2", "value": "New Value 2"},
],
},
{
"document_id": "def456",
"fields": [{"name": "FieldName3", "value": "New Value 3"}],
},
],
],
),
],
) -> UpdateDocumentFieldsResponse:
"""Update text fields in multiple documents.
This tool updates text fields in multiple documents using the SignNow API.
Only text fields can be updated using the prefill_text_fields endpoint.
IMPORTANT: This tool works only with individual documents, not document groups.
To find out what fields are available in a document or document group,
use the get_document tool first.
Args:
update_requests: Array of UpdateDocumentFields with document IDs and fields to update
Returns:
UpdateDocumentFieldsResponse with results for each document update
"""
token, client = _get_token_and_client(token_provider)
# Initialize client and use the imported function from document module
return _update_document_fields(client, token, update_requests)
# @mcp.tool(
# name="upload_document",
# description="Upload a document to SignNow",
# tags=["document", "upload", "file"]
# )
# def upload_document(
# ctx: Context,
# file_content: Annotated[bytes, Field(description="Document file content as bytes")],
# filename: Annotated[str, Field(description="Name of the file to upload")],
# check_fields: Annotated[bool, Field(description="Whether to check for fields in the document (default: True)")] = True
# ) -> UploadDocumentResponse:
# """Upload a document to SignNow.
# This tool uploads a document file to SignNow and returns the document ID.
# The uploaded document can then be used for signing workflows.
# Args:
# file_content: Document file content as bytes
# filename: Name of the file to upload
# check_fields: Whether to check for fields in the document (default: True)
# Returns:
# UploadDocumentResponse with uploaded document ID, filename, and check_fields status
# """
# headers = get_http_headers()
# token = token_provider.get_access_token(headers)
# if not token:
# raise ValueError("No access token available")
# # Initialize client and use the imported function from upload_document module
# client = SignNowAPIClient(token_provider.signnow_config)
# return _upload_document(file_content, filename, check_fields, token, client)
return