Skip to main content
Glama
abhineet34

linkedin-mcp-server

Upload LinkedIn Image

linkedin_upload_image

Upload an image to LinkedIn to obtain an image URN for creating image posts. Handles initialization and upload automatically.

Instructions

Upload an image to LinkedIn and get an image URN to use when creating an image post.

This is a two-step process handled automatically:

  1. Initialize the upload via /rest/images to get a pre-signed upload URL

  2. PUT the image binary to that URL

After upload, use the returned image URN as image_asset_urn in linkedin_create_post.

Requires scope: w_member_social

Args:

  • author_urn (string): Owner URN — 'urn:li:person:{id}' from linkedin_get_profile

  • image_base64 (string): Base64-encoded image file contents

  • mime_type ('image/jpeg' | 'image/png' | 'image/gif'): Image MIME type (default: image/jpeg)

Returns: { "asset_urn": string, // e.g., "urn:li:image:C5622AQH..." "upload_url": string // The URL that was used for upload (informational) }

Examples:

  • Use when: "Post an image to LinkedIn" → upload first, then create_post with asset URN

  • Don't use when: You only want a text post (image upload not needed)

Error Handling:

  • 403 if w_member_social scope is not granted

  • 400 if the author_urn format is invalid

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
author_urnYesURN of the owner of this asset. Use 'urn:li:person:{id}' for members. Get your ID from linkedin_get_profile's 'sub' field.
image_base64YesBase64-encoded image data (JPEG or PNG)
mime_typeNoMIME type of the image (default: image/jpeg)image/jpeg

Implementation Reference

  • Registration of the 'linkedin_upload_image' tool on the MCP server via server.registerTool(...)
    export function registerMediaTools(server: McpServer): void {
      server.registerTool(
        "linkedin_upload_image",
        {
          title: "Upload LinkedIn Image",
          description: `Upload an image to LinkedIn and get an image URN to use when creating an image post.
    
    This is a two-step process handled automatically:
      1. Initialize the upload via /rest/images to get a pre-signed upload URL
      2. PUT the image binary to that URL
    
    After upload, use the returned image URN as image_asset_urn in linkedin_create_post.
    
    Requires scope: w_member_social
    
    Args:
      - author_urn (string): Owner URN — 'urn:li:person:{id}' from linkedin_get_profile
      - image_base64 (string): Base64-encoded image file contents
      - mime_type ('image/jpeg' | 'image/png' | 'image/gif'): Image MIME type (default: image/jpeg)
    
    Returns:
      {
        "asset_urn": string,  // e.g., "urn:li:image:C5622AQH..."
        "upload_url": string  // The URL that was used for upload (informational)
      }
    
    Examples:
      - Use when: "Post an image to LinkedIn" → upload first, then create_post with asset URN
      - Don't use when: You only want a text post (image upload not needed)
    
    Error Handling:
      - 403 if w_member_social scope is not granted
      - 400 if the author_urn format is invalid`,
          inputSchema: UploadImageInputSchema,
          annotations: {
            readOnlyHint: false,
            destructiveHint: false,
            idempotentHint: false,
            openWorldHint: true,
          },
        },
        async (params: UploadImageInput) => {
          try {
            // Step 1: Initialize the upload via the modern /rest/images endpoint.
            // This returns an image URN (urn:li:image:...) that is compatible with
            // the versioned /rest/posts API. The legacy /v2/assets endpoint returns
            // urn:li:digitalmediaAsset:... which is rejected by /rest/posts.
            const initBody = {
              initializeUploadRequest: {
                owner: params.author_urn,
              },
            };
    
            const initResponse = await restPost<{
              value: {
                uploadUrl: string;
                image: string;
                uploadUrlExpiresAt?: number;
              };
            }>("/images?action=initializeUpload", initBody);
    
            const imageUrn = initResponse.value.image;
            const uploadUrl = initResponse.value.uploadUrl;
    
            // Step 2: PUT the image binary to the pre-signed URL
            const imageBuffer = Buffer.from(params.image_base64, "base64");
            await uploadBinaryToUrl(uploadUrl, imageBuffer, params.mime_type);
    
            const result = { asset_urn: imageUrn, upload_url: uploadUrl };
    
            return {
              content: [
                {
                  type: "text",
                  text: [
                    "Image uploaded successfully.",
                    "",
                    `**Image URN:** ${imageUrn}`,
                    "",
                    "Use this URN as the `image_asset_urn` parameter in `linkedin_create_post`.",
                  ].join("\n"),
                },
              ],
              structuredContent: result,
            };
          } catch (error) {
            return { content: [{ type: "text", text: handleApiError(error) }] };
          }
        }
      );
  • Handler function that: (1) calls /images?action=initializeUpload to get a pre-signed URL and image URN, (2) PUTs the base64-decoded image binary to that URL via uploadBinaryToUrl, (3) returns the image URN for use in linkedin_create_post
      async (params: UploadImageInput) => {
        try {
          // Step 1: Initialize the upload via the modern /rest/images endpoint.
          // This returns an image URN (urn:li:image:...) that is compatible with
          // the versioned /rest/posts API. The legacy /v2/assets endpoint returns
          // urn:li:digitalmediaAsset:... which is rejected by /rest/posts.
          const initBody = {
            initializeUploadRequest: {
              owner: params.author_urn,
            },
          };
    
          const initResponse = await restPost<{
            value: {
              uploadUrl: string;
              image: string;
              uploadUrlExpiresAt?: number;
            };
          }>("/images?action=initializeUpload", initBody);
    
          const imageUrn = initResponse.value.image;
          const uploadUrl = initResponse.value.uploadUrl;
    
          // Step 2: PUT the image binary to the pre-signed URL
          const imageBuffer = Buffer.from(params.image_base64, "base64");
          await uploadBinaryToUrl(uploadUrl, imageBuffer, params.mime_type);
    
          const result = { asset_urn: imageUrn, upload_url: uploadUrl };
    
          return {
            content: [
              {
                type: "text",
                text: [
                  "Image uploaded successfully.",
                  "",
                  `**Image URN:** ${imageUrn}`,
                  "",
                  "Use this URN as the `image_asset_urn` parameter in `linkedin_create_post`.",
                ].join("\n"),
              },
            ],
            structuredContent: result,
          };
        } catch (error) {
          return { content: [{ type: "text", text: handleApiError(error) }] };
        }
      }
    );
  • Zod input schema for linkedin_upload_image: validates author_urn (string), image_base64 (string), and MIME type (enum with default)
    const UploadImageInputSchema = z
      .object({
        author_urn: z
          .string()
          .describe(
            "URN of the owner of this asset. Use 'urn:li:person:{id}' for members. " +
              "Get your ID from linkedin_get_profile's 'sub' field."
          ),
        image_base64: z
          .string()
          .describe("Base64-encoded image data (JPEG or PNG)"),
        mime_type: z
          .enum(["image/jpeg", "image/png", "image/gif"])
          .default("image/jpeg")
          .describe("MIME type of the image (default: image/jpeg)"),
      })
      .strict();
  • Helper function that performs the binary PUT upload to the pre-signed URL returned by LinkedIn's image initialization endpoint
    export async function uploadBinaryToUrl(
      uploadUrl: string,
      data: Buffer,
      contentType: string
    ): Promise<void> {
      await axios.put(uploadUrl, data, {
        headers: {
          Authorization: `Bearer ${getAccessToken()}`,
          "Content-Type": contentType,
        },
        timeout: 60000,
      });
    }
  • src/index.ts:22-33 (registration)
    Main entry point imports and invokes registerMediaTools, which registers linkedin_upload_image on the MCP server
    import { registerMediaTools } from "./tools/media.js";
    import { registerOrganizationTools } from "./tools/organizations.js";
    
    const server = new McpServer({
      name: "linkedin-mcp-server",
      version: "1.0.0",
    });
    
    registerProfileTools(server);
    registerPostTools(server);
    registerMediaTools(server);
    registerOrganizationTools(server);
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses the upload process (two steps), required scope w_member_scope, and error handling (403, 400). Adds behavioral context beyond annotations (which only indicate non-readonly, non-destructive, open-world). No contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with clear sections (description, process, args, returns, examples, errors). Every sentence is informative and earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Comprehensive coverage of return values, error scenarios, and prerequisites despite no output schema. The two-step upload complexity is fully described, making it complete for agent invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema coverage, description adds value by specifying author_urn format from linkedin_get_profile, base64 encoding, and default mime_type. Examples and return shape further clarify parameter usage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the tool uploads an image to LinkedIn and returns an image URN for use in creating an image post. Distinguishes from sibling linkedin_create_post by explaining it's a prerequisite step.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly provides when-to-use ('Post an image to LinkedIn') and when-not-to-use ('only a text post'), names the alternative, and describes the two-step process handled automatically.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/abhineet34/linkedin-mcp-server'

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