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).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues