Skip to main content
Glama
zecloud

linkedin-mcp-oauth

by zecloud
README.md
# LinkedIn MCP Server (Azure Functions)

A **remote [Model Context Protocol](https://modelcontextprotocol.io/) server** for LinkedIn, hosted on **Azure Functions** using the official [Azure Functions MCP extension](https://github.com/Azure/azure-functions-mcp-extension). It exposes personal LinkedIn tools to MCP clients such as GitHub Copilot (VS Code) and Claude Desktop.

## Tools

| Tool | Description |
| --- | --- |
| `get_my_profile` | Returns the authenticated owner's LinkedIn profile (`GET /v2/userinfo`, OpenID Connect). |
| `create_post` | Publishes a **public post** on the owner's LinkedIn feed (`POST /v2/ugcPosts`, scope `w_member_social`). Accepts `text` (required) and `imageUrl` (optional public `https://` URL to a JPG/PNG/GIF ≤ 8 MB — the server downloads it and uploads it via `POST /v2/images?action=initializeUpload` + binary `PUT`). |
| `get_auth_status` | Reports token validity, expiration dates and available scopes. |

## Architecture

```mermaid
flowchart LR
    subgraph Client
        C[Copilot / MCP client]
    end
    subgraph "Azure Functions (Flex Consumption)"
        M[mcpTool triggers<br/>get_my_profile · create_post · get_auth_status]
        L[GET /auth/login]
        CB[GET /auth/callback]
        TS[(Table Storage<br/>tokens)]
    end
    C -- "streamable HTTP /runtime/webhooks/mcp" --> M
    L -- 302 --> LI[LinkedIn OAuth]
    LI -- code --> CB
    CB --> TS
    M --> TS
    M --> API[api.linkedin.com]
```

**Why TypeScript?** The MCP extension supports several stacks, but TypeScript (`app.mcpTool`, `@azure/functions` ≥ 4.9.0 + extension bundle `[4.0.0, 5.0.0)`) is first-class, requires no worker-specific package, and matches the Node.js ecosystem this project is inspired by. C# isolated (`Microsoft.Azure.Functions.Worker.Extensions.Mcp`) would be the alternative for .NET teams.

**OAuth**: LinkedIn's member APIs (`userinfo`, `ugcPosts`) **do not support `client_credentials`**. A one-time 3-legged authorization-code flow is required: open `/auth/login`, consent in the browser, and the server stores the access token (60 days) + refresh token (1 year) in Azure Table Storage. The access token is refreshed automatically whenever it has less than 7 days left.

## 1. Create the LinkedIn app

1. Go to the [LinkedIn Developer Portal](https://developer.linkedin.com/) → **Create app**.
2. In the app **Products** tab, request/enable:
   - **Sign In with LinkedIn using OpenID Connect** (scopes `openid`, `profile`)
   - **Share on LinkedIn** (scope `w_member_social`)
3. In the **Auth** tab, add the redirect URL:
   - Local: `http://localhost:7071/auth/callback`
   - Azure (after deploy): `https://<your-function-app>.azurewebsites.net/auth/callback`
4. Note the **Client ID** and **Client Secret**.

## 2. Run locally

Prerequisites: Node.js 20+, [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local) v4.0.7030+, and [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) (for Table Storage).

```bash
npm install
cp local.settings.json.template local.settings.json   # fill in LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET
azurite --silent &                                     # or use a real storage account connection string
npm run build
func start
```

Then open <http://localhost:7071/auth/login> in a browser and complete the LinkedIn consent once.

### Connect a client locally

`.vscode/mcp.json` (or your client's equivalent):

```json
{
  "servers": {
    "linkedin-local": {
      "type": "http",
      "url": "http://localhost:7071/runtime/webhooks/mcp"
    }
  }
}
```

## 3. Deploy to Azure

Prerequisites: [Azure Developer CLI (azd)](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd).

```bash
azd init            # or run inside this folder directly
azd env set LINKEDIN_CLIENT_ID <your-client-id>       # optional — see below
azd env set LINKEDIN_CLIENT_SECRET <your-client-secret> # optional — see below
azd up
```

The Bicep parameters `linkedInClientId` / `linkedInClientSecret` are optional (default `''`). If you skip the `azd env set` commands, deploy first and then set `LINKEDIN_CLIENT_ID` and `LINKEDIN_CLIENT_SECRET` manually in the Azure Portal (**Function App → Settings → Environment variables**). The `/auth/login` and `/auth/callback` endpoints return an explicit 500 error listing any missing app settings until they are configured.

This provisions (Bicep, `infra/`): a resource group, a Storage account (tokens table + host storage), Log Analytics + Application Insights, and a **Flex Consumption (FC1)** Function App running Node.js 22 — no VNet, no private endpoints, no Entra app.

After deployment:

1. Add `https://<your-function-app>.azurewebsites.net/auth/callback` as a redirect URL in the LinkedIn app (Auth tab).
2. Open `https://<your-function-app>.azurewebsites.net/auth/login` and complete the OAuth flow.
3. Retrieve the MCP system key:

   ```bash
   az functionapp keys list --resource-group rg-<env-name> --name <function-app-name> --query systemKeys.mcp_extension --output tsv
   ```

4. Configure your MCP client:

   ```json
   {
     "inputs": [
       { "type": "promptString", "id": "mcp-key", "description": "MCP extension system key", "password": true }
     ],
     "servers": {
       "linkedin": {
         "type": "http",
         "url": "https://<your-function-app>.azurewebsites.net/runtime/webhooks/mcp",
         "headers": { "x-functions-key": "${input:mcp-key}" }
       }
     }
   }
   ```

## Configuration

| App setting | Description |
| --- | --- |
| `LINKEDIN_CLIENT_ID` | LinkedIn app client ID. |
| `LINKEDIN_CLIENT_SECRET` | LinkedIn app client secret. For production, prefer a [Key Vault reference](https://learn.microsoft.com/azure/app-service/app-service-key-vault-references) (`@Microsoft.KeyVault(...)`) instead of a plain value. |
| `BASE_URL` | Public base URL of the app, used to build the OAuth redirect URI. Set automatically by Bicep in Azure. |
| `AzureWebJobsStorage` | Storage connection string — also used for the `tokens` table. |

## Project structure

```
├── host.json                  # MCP extension config (serverName, instructions) + extension bundle
├── package.json / tsconfig.json
├── azure.yaml                 # azd service definition
├── local.settings.json.template
├── src/
│   ├── index.ts
│   ├── functions/
│   │   ├── mcpTools.ts        # app.mcpTool: get_my_profile, create_post, get_auth_status
│   │   └── auth.ts            # GET /auth/login, GET /auth/callback (OAuth 3-legged)
│   └── lib/
│       ├── tokenStore.ts      # Azure Table Storage tokens + automatic refresh (< 7 days)
│       └── linkedinClient.ts  # /v2/userinfo, /v2/ugcPosts
└── infra/
    ├── main.bicep             # Flex Consumption + Storage + App Insights
    └── main.parameters.json
```

## Credits & license

Inspired by [Dishant27/linkedin-mcp-server](https://github.com/Dishant27/linkedin-mcp-server) (LinkedIn API usage patterns). This implementation is written from scratch around the Azure Functions MCP extension and 3-legged OAuth. Licensed under [MIT](LICENSE).