@postman/postman-mcp-server
Version:
Postman MCP Server — connect AI agents (Claude Code, Cursor, VS Code Copilot, Gemini CLI) to your Postman collections, specifications, and environments via Model Context Protocol (MCP)
208 lines (199 loc) • 9.32 kB
JavaScript
import { z } from 'zod';
import { ContentType } from '../clients/postman.js';
import { asMcpError, McpError, defineToolAnnotations } from './utils/toolHelpers.js';
export const method = 'searchPostmanElements';
export const title = 'Search Postman elements';
export const description = `Search for Postman entities (requests, collections, workspaces, specs, flows, environments, mocks, and documents).
**Ownership:**
- \`organization\` — Search within all resources owned by your organization (default).
- \`external\` — Search within the public Postman network (third-party and community APIs).
- \`all\` — Search across all scopes.
**When to use each ownership value and filters:**
| Goal | Recommended approach |
|------|----------------------|
| Find an internal API (e.g. "our notification service") | \`ownership: organization\` |
| Find a trusted API published to the Private Network | \`ownership: organization\` + \`privateNetwork: true\` filter |
| Find an internal API in all resources of organization and are visible to the organization only | \`ownership: organization\` + \`visibility: internal\` filter |
| Find an API by your organization that is made publicly visible | \`ownership: organization\` + \`visibility: public\` filter |
| Find a third party publicly visible API (e.g. "Stripe API", "Twilio API") | \`ownership: external\` + \`visibility: public\` filter |
| User says "our APIs", "internal", "team" | \`ownership: organization\` |
| Search across all scopes | \`ownership: all\` |
**Element Types:**
- \`requests\`: Search for individual API requests.
- \`collections\`: Search for API collections.
- \`workspaces\`: Search for Postman workspaces.
- \`specs\`: Search for API specifications.
- \`flows\`: Search for Postman Flows.
- \`environments\`: Search for Postman Environments.
- \`mocks\`: Search for Postman Mock Servers.
- \`documents\`: Search for Postman workspace documents.
**Filters:**
Use the \`filters\` parameter to narrow results. The top-level key must be \`$and\` with an array of condition objects. Each condition object must contain exactly one field key.
Supported filter fields:
| Field | Operators | Notes |
|-------|-----------|-------|
| \`workspaceId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | All element types. \`$in\`/\`$nin\` accept arrays. |
| \`collectionId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Requests and collections only. |
| \`visibility\` | \`$eq\`, \`$ne\` | Values: \`public\`, \`partner\`, \`internal\`. All element types. |
| \`privateNetwork\` | \`$eq\`, \`$ne\` | Boolean. All element types. |
| \`publisherIsVerified\` | \`$eq\`, \`$ne\` | Boolean. All element types. |
| \`method\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | HTTP methods (GET, POST, etc.). Requests only. |
| \`tags\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Workspaces and collections only. |
| \`requestId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Requests only. |
| \`specificationId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Specs only. |
| \`flowId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Flows only. |
| \`documentId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Documents only. |
| \`createdBy\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | All element types. |
| \`organizationId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | All element types. |
| \`teamId\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | All element types. |
| \`isGitConnected\` | \`$eq\`, \`$ne\` | Boolean. Workspaces, collections, requests, specs, flows, environments, mocks, documents. |
| \`type\` | \`$eq\`, \`$ne\`, \`$in\`, \`$nin\` | Requests only. |
**Filter examples:**
- Private API Network only: \`{"$and":[{"privateNetwork":{"$eq":true}}]}\`
- Single workspace: \`{"$and":[{"workspaceId":{"$eq":"ws-abc123"}}]}\`
- Multiple workspaces: \`{"$and":[{"workspaceId":{"$in":["ws-1","ws-2"]}}]}\`
- Public visibility: \`{"$and":[{"visibility":{"$eq":"public"}}]}\`
- GET requests only: \`{"$and":[{"method":{"$eq":"GET"}}]}\`
- Combine conditions: \`{"$and":[{"visibility":{"$eq":"public"}},{"workspaceId":{"$eq":"ws-abc123"}}]}\`
- Environments in a workspace: \`{"$and":[{"workspaceId":{"$eq":"ws-abc123"}}]}\``;
const booleanOperator = z
.object({
$eq: z.boolean().optional(),
$ne: z.boolean().optional(),
})
.refine((obj) => Object.keys(obj).length === 1, {
message: 'Exactly one of $eq or $ne must be provided.',
});
const stringOperator = z
.object({
$eq: z.string().min(1).optional(),
$ne: z.string().min(1).optional(),
$in: z.array(z.string().min(1)).min(1).optional(),
$nin: z.array(z.string().min(1)).min(1).optional(),
})
.refine((obj) => Object.keys(obj).length === 1, {
message: 'Exactly one of $eq, $ne, $in, or $nin must be provided.',
});
const visibilityOperator = z
.object({
$eq: z.enum(['internal', 'public', 'partner']).optional(),
$ne: z.enum(['internal', 'public', 'partner']).optional(),
})
.refine((obj) => Object.keys(obj).length === 1, {
message: 'Exactly one of $eq or $ne must be provided.',
});
const filterCondition = z
.object({
privateNetwork: booleanOperator.optional(),
publisherIsVerified: booleanOperator.optional(),
isGitConnected: booleanOperator.optional(),
visibility: visibilityOperator.optional(),
workspaceId: stringOperator.optional(),
collectionId: stringOperator.optional(),
method: stringOperator.optional(),
tags: stringOperator.optional(),
requestId: stringOperator.optional(),
specificationId: stringOperator.optional(),
flowId: stringOperator.optional(),
documentId: stringOperator.optional(),
createdBy: stringOperator.optional(),
organizationId: stringOperator.optional(),
teamId: stringOperator.optional(),
type: stringOperator.optional(),
})
.refine((obj) => Object.values(obj).filter((v) => v !== undefined).length === 1, {
message: 'Each filter condition must contain exactly one field.',
});
const filtersSchema = z
.object({
$and: z
.array(filterCondition)
.min(1)
.describe('Array of filter conditions. Each condition must contain exactly one field key mapped to an operator object.'),
})
.describe('Structured filter expression. Top-level key must be "$and" with an array of condition objects. ' +
'Each condition: { "<field>": { "<operator>": <value> } }. ' +
'Example: {"$and":[{"privateNetwork":{"$eq":true}}]}');
export const parameters = z.object({
entityType: z
.enum([
'requests',
'collections',
'workspaces',
'specs',
'flows',
'environments',
'mocks',
'documents',
])
.describe('The type of Postman entity to search for: `requests` (individual API requests), `collections` (API collections), `workspaces` (Postman workspaces), `specs` (API specifications), `flows` (Postman Flows), `environments` (Postman Environments), `mocks` (Postman Mock Servers), or `documents` (Postman workspace documents).')
.default('requests'),
q: z
.string()
.max(512)
.describe('The search query (e.g. "payment API", "notification service", "Stripe").')
.optional(),
ownership: z
.enum(['organization', 'external', 'all'])
.describe('The ownership scope. Use `organization` to search all resources in your organization (default), `external` to search the public Postman network, or `all` to search across all scopes.')
.default('organization'),
filters: filtersSchema.optional(),
cursor: z
.string()
.describe('The cursor to get the next set of results in the paginated response. Pass the `nextCursor` value from the previous response.')
.optional(),
limit: z
.number()
.int()
.gte(1)
.lte(25)
.describe('The maximum number of search results to return. Maximum: 25.')
.default(10)
.optional(),
});
export const annotations = defineToolAnnotations({
title: 'Search Postman elements',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
});
export async function handler(args, extra) {
try {
const query = new URLSearchParams();
if (args.limit !== undefined)
query.set('limit', String(args.limit));
if (args.cursor !== undefined)
query.set('nextCursor', String(args.cursor));
const endpoint = query.toString() ? `/search?${query.toString()}` : '/search';
const body = {
ownership: args.ownership,
elementType: args.entityType,
};
if (args.q !== undefined) {
body.q = args.q;
}
if (args.filters !== undefined) {
body.filters = args.filters;
}
const result = await extra.client.post(endpoint, {
body: JSON.stringify(body),
contentType: ContentType.Json,
headers: extra.headers,
});
return {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
},
],
};
}
catch (e) {
if (e instanceof McpError) {
throw e;
}
throw asMcpError(e);
}
}