Guide · Cloud Storage
MCP Tools for Azure Blob Storage — managed identity, SAS tokens, block blobs, large file upload
Three Azure Blob Storage behaviours catch developers building MCP storage tools: connection strings encode the storage account key and must not be used in production — the storage account key grants unrestricted access to all containers and blobs; production code should use DefaultAzureCredential (managed identity on Azure, service principal env vars locally), and the common "Server failed to authenticate the request" error from using a connection string in the wrong environment almost always means the connection string belongs to a different storage account or was copy-pasted with line-break corruption; SAS tokens must be constructed from the correct resource type hierarchy — an Account SAS grants access to the storage account as a whole, a Service SAS is scoped to a container or blob, and a User Delegation SAS requires an Azure AD token; mixing these up generates a SAS that fails with "Signature did not match" even though the token looks valid, because the signed fields differ between types; and blob type is immutable after creation — Block blobs (default), Append blobs, and Page blobs cannot be converted between types; MCP servers should always use Block blobs for general object storage, since Page blobs are for VHD disks and Append blobs are for log streams only.
TL;DR
Client: new BlobServiceClient(url, new DefaultAzureCredential()). Container: serviceClient.getContainerClient(name). Upload: containerClient.getBlockBlobClient(key).uploadData(buffer, { blobHTTPHeaders: { blobContentType } }). Download: blockBlobClient.downloadToBuffer(). SAS: generateBlobSASQueryParameters({ ... }, sharedKeyCredential).toString(). Health: containerClient.exists() — true = accessible, AuthorizationPermissionMismatch = no permission.
Authentication — DefaultAzureCredential vs connection string
DefaultAzureCredential from @azure/identity resolves credentials in this order: AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/AZURE_TENANT_ID env vars (service principal) → workload identity → managed identity → Azure CLI token → Visual Studio Code token. On Azure VMs, AKS, Container Apps, and App Service with managed identity enabled, no env vars are needed — the credential resolves from the managed identity endpoint. The "No credential providers available" error means none of these sources are configured.
import { BlobServiceClient, ContainerClient, BlockBlobClient,
generateBlobSASQueryParameters, BlobSASPermissions,
StorageSharedKeyCredential } from '@azure/storage-blob';
import { DefaultAzureCredential } from '@azure/identity';
const STORAGE_ACCOUNT = process.env.AZURE_STORAGE_ACCOUNT!;
const CONTAINER = process.env.AZURE_STORAGE_CONTAINER!;
// Production: managed identity or service principal (no secrets in code)
const credential = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
`https://${STORAGE_ACCOUNT}.blob.core.windows.net`,
credential
);
const containerClient = blobServiceClient.getContainerClient(CONTAINER);
// Development: connection string (avoid in production — encodes account key)
// const blobServiceClient = BlobServiceClient.fromConnectionString(
// process.env.AZURE_STORAGE_CONNECTION_STRING!
// );
// Shared key credential — needed to generate SAS tokens programmatically
// Store account key in Key Vault and fetch at startup, not in env vars
const sharedKeyCredential = new StorageSharedKeyCredential(
STORAGE_ACCOUNT,
process.env.AZURE_STORAGE_ACCOUNT_KEY! // from Key Vault in production
);
Azure RBAC roles for Blob Storage: Storage Blob Data Reader for read-only access, Storage Blob Data Contributor for read-write, Storage Blob Data Owner for full access including ACLs. These are separate from the Contributor management-plane role — an Azure subscription Contributor can manage the storage account but cannot read blob data without a data-plane role. This distinction causes confusion when "access denied" errors appear for users who can see the container in the portal (management plane access) but can't download blobs (data plane).
Uploading blobs — block upload, streaming, and parallel upload
BlockBlobClient.uploadData() accepts a Buffer, Blob, ArrayBuffer, or ArrayBufferView and automatically uses parallel block upload for large payloads. The default blockSize is 4 MB and the default concurrency is 5 (5 parallel block uploads). For streaming data where total size is unknown, use uploadStream() which accepts a Readable. Content type must be set explicitly via blobHTTPHeaders — Azure Blob Storage does not infer MIME type from file extension, and defaults to application/octet-stream if omitted, which breaks browser rendering of images and HTML.
// Upload Buffer — auto parallel blocks for large data
async function uploadBuffer(key: string, data: Buffer, contentType: string) {
const blockBlobClient = containerClient.getBlockBlobClient(key);
const response = await blockBlobClient.uploadData(data, {
blobHTTPHeaders: { blobContentType: contentType },
metadata: { uploadedBy: 'mcp-server', sessionId: 'sess_abc' },
// For large buffers, tune block size and concurrency:
blockSize: 4 * 1024 * 1024, // 4 MB blocks
concurrency: 4, // 4 parallel block uploads
onProgress: (progress) => {
console.log(`Uploaded ${progress.loadedBytes} bytes`);
},
});
return { etag: response.etag, url: blockBlobClient.url };
}
// Upload from a stream (pipe through without buffering in memory)
import { Readable } from 'stream';
async function uploadStream(key: string, stream: Readable, contentType: string) {
const blockBlobClient = containerClient.getBlockBlobClient(key);
// bufferSize controls max memory per block — keep low for memory-constrained servers
await blockBlobClient.uploadStream(stream, 4 * 1024 * 1024, 4, {
blobHTTPHeaders: { blobContentType: contentType },
});
}
// Overwrite-safe upload — only write if blob does not exist (conditional PUT)
async function uploadIfNotExists(key: string, data: Buffer, contentType: string) {
const blockBlobClient = containerClient.getBlockBlobClient(key);
try {
await blockBlobClient.uploadData(data, {
blobHTTPHeaders: { blobContentType: contentType },
conditions: { ifNoneMatch: '*' }, // fails with 409 if blob already exists
});
return { created: true };
} catch (err: any) {
if (err.statusCode === 409) return { created: false };
throw err;
}
}
SAS tokens — service SAS vs user delegation SAS
Service SAS tokens are signed with the storage account key and immediately reflect the current permissions without needing Azure AD. User delegation SAS tokens are signed with a temporary Azure AD-derived key obtained via getUserDelegationKey() and are preferred because the signing key comes from Azure AD (auditable, short-lived). Service SAS requires the storage account key to be available in the server process; User Delegation SAS requires the server identity to have Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action permission.
import { generateBlobSASQueryParameters, BlobSASPermissions, SASProtocol } from '@azure/storage-blob';
// Service SAS — signed with account key (requires sharedKeyCredential)
async function getBlobDownloadSAS(containerName: string, blobName: string, expiryMinutes = 60): Promise {
const sasOptions = {
containerName,
blobName,
startsOn: new Date(),
expiresOn: new Date(Date.now() + expiryMinutes * 60 * 1000),
permissions: BlobSASPermissions.parse('r'), // read-only
protocol: SASProtocol.Https,
};
const sasToken = generateBlobSASQueryParameters(sasOptions, sharedKeyCredential).toString();
return `https://${STORAGE_ACCOUNT}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`;
}
// Service SAS for upload (write permission)
async function getBlobUploadSAS(containerName: string, blobName: string, expiryMinutes = 15): Promise {
const sasOptions = {
containerName,
blobName,
startsOn: new Date(),
expiresOn: new Date(Date.now() + expiryMinutes * 60 * 1000),
permissions: BlobSASPermissions.parse('cw'), // create + write
protocol: SASProtocol.Https,
};
const sasToken = generateBlobSASQueryParameters(sasOptions, sharedKeyCredential).toString();
return `https://${STORAGE_ACCOUNT}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`;
}
// User Delegation SAS — signed with a temporary Azure AD key (no account key needed)
async function getUserDelegationSAS(containerName: string, blobName: string): Promise {
const startsOn = new Date();
const expiresOn = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
// Get user delegation key — valid up to 7 days
const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
const sasToken = generateBlobSASQueryParameters(
{
containerName,
blobName,
startsOn,
expiresOn,
permissions: BlobSASPermissions.parse('r'),
protocol: SASProtocol.Https,
},
userDelegationKey,
STORAGE_ACCOUNT
).toString();
return `https://${STORAGE_ACCOUNT}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`;
}
SAS token maximum expiry: Service SAS can be set to any future date (effectively unlimited, though auditing suffers). User Delegation SAS expires with the delegation key — maximum 7 days. Account SAS (for service-level access including blob, queue, table, file) should be avoided in favour of the more narrowly scoped Service SAS. A SAS token with the wrong signedService field (e.g., created for the queue service but used for blob) returns "Signature did not match" — check the ss query param.
Downloading and listing blobs
Azure Blob Storage downloads support byte-range requests natively — pass offset and count to download() to fetch a specific byte range. This is useful for MCP tools that serve partial content (audio/video streaming, resumable downloads). Listing blobs uses a page iterator; the SDK returns pages of up to 5000 blobs by default and provides an async iterator for convenience.
// Download blob to Buffer
async function downloadBuffer(key: string): Promise {
const blockBlobClient = containerClient.getBlockBlobClient(key);
return blockBlobClient.downloadToBuffer();
}
// Download a byte range (e.g., first 4 KB for header inspection)
async function downloadRange(key: string, offset: number, length: number): Promise {
const blockBlobClient = containerClient.getBlockBlobClient(key);
return blockBlobClient.downloadToBuffer(offset, length);
}
// List blobs with prefix filter (paginates automatically)
async function listBlobs(prefix: string): Promise> {
const blobs: Array<{ name: string; size: number; lastModified: Date }> = [];
const iter = containerClient.listBlobsFlat({ prefix });
for await (const blob of iter) {
blobs.push({
name: blob.name,
size: blob.properties.contentLength ?? 0,
lastModified: blob.properties.lastModified!,
});
}
return blobs;
}
// List virtual directories (hierarchical listing)
async function listDirectory(prefix: string) {
const items = { folders: [] as string[], files: [] as string[] };
const iter = containerClient.listBlobsByHierarchy('/', { prefix });
for await (const item of iter) {
if (item.kind === 'prefix') items.folders.push(item.name);
else items.files.push(item.name);
}
return items;
}
Health probe — container existence and write permission
containerClient.exists() returns true if the container exists and the caller has storage.blobs.list (equivalent) permission. The error distinctions: 403 with AuthorizationPermissionMismatch means the managed identity or service principal exists but lacks the data-plane RBAC role; 403 with AuthorizationFailure or AuthenticationFailed usually means the credential is wrong or expired; 404 means the container does not exist.
async function checkAzureBlobHealth(): Promise
AliveMCP tracks Azure Blob-backed MCP endpoints by monitoring the health probe response latency trend. A sudden latency increase on Azure Blob operations often signals a networking issue (private endpoint DNS resolution failure, VNet peering drop) rather than an authentication problem — the health probe response code stays 200 but latency jumps from ~20 ms to 2–5 seconds.
Related guides
- MCP Tools for AWS S3 — presigned URLs, multipart upload, credential chain, lifecycle rules
- MCP Tools for Google Cloud Storage — ADC credentials, signed URLs, uniform bucket access
- MCP Tools for Cloudflare R2 — S3 API compatibility, Workers binding, egress-free storage
- MCP Tools for MinIO — self-hosted S3 API, bucket policies, presigned URL generation
- MCP server health check patterns
- MCP server uptime monitoring