Skip to main content
Glama
signnow.py43.4 kB
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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mihasicehcek/sn-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server