initial check-in for sync support

1.Remove sync related ui limitation for MCP client
2.Initial support for cloud sync

add basic parser

add missing attribute

revert changes

use tailwind v4

1.update mcp client types

add a new type naming for insomnia mcp git files

add test and fix export issue

add mcp yaml export test

update logic to detect insomnia supported yaml files

fix type issue
This commit is contained in:
Kent Wang
2025-11-22 20:36:22 +08:00
parent 1c1c7b4179
commit e2fb1bcbbd
18 changed files with 331 additions and 51 deletions

View File

@@ -12,6 +12,8 @@ import {
JsonSchema,
KeyLiteralSchema,
LiteralSchema,
McpClientSchema,
McpRequestSchema,
MetaGroupSchema,
MetaSchema,
MockRouteSchema,
@@ -131,6 +133,15 @@ const makeMockRoute = (overrides: Record<string, unknown> = {}) => ({
...overrides,
});
const makeMcpRequest = (overrides: Record<string, unknown> = {}) => ({
name: 'MCP Request',
url: 'https://example.com/mcp',
transportType: 'streamable-http',
headers: [{ name: 'X-MCP', value: '1' }],
authentication: { type: 'none' },
...overrides,
});
// -----------------------------
// Primitive & Base Schemas
// -----------------------------
@@ -424,6 +435,39 @@ describe('MockRouteSchema', () => {
});
});
// -----------------------------
// Mcp Request
// -----------------------------
describe('McpRequestSchema', () => {
it('parses mcp request schmea and applies defaults', () => {
const mcpRequestData = makeMcpRequest({
env: [
{
id: 'env-1',
name: 'foo',
value: 'bar',
type: 'str',
enabled: true,
},
],
roots: [
{
uri: '/data/to/root/file',
},
{
uri: '/more/data',
},
],
});
const r = McpRequestSchema.parse(mcpRequestData);
expect(r.transportType).toBe(mcpRequestData.transportType);
expect(r.url).toBe(mcpRequestData.url);
expect(r.env?.[0].name).toBe('foo');
expect(r.roots?.[0].uri).toBe('/data/to/root/file');
});
});
// -----------------------------
// Top-level documents
// -----------------------------
@@ -476,6 +520,19 @@ describe('MockServerSchema (top-level)', () => {
});
});
describe('McpClientSchema (top-level)', () => {
it('parse mcp workspace with mcp request ', () => {
const mcpRequestData = makeMcpRequest();
const mcpClient = McpClientSchema.parse({
type: 'mcpClient.insomnia/5.0',
name: 'MCP Client',
mcpRequest: mcpRequestData,
});
expect(mcpClient.mcpRequest?.url).toBe(mcpRequestData.url);
expect(mcpClient.mcpRequest?.transportType).toBe(mcpRequestData.transportType);
});
});
describe('InsomniaFileSchema (discriminated union)', () => {
it('accepts collection variant', () => {
const f = InsomniaFileSchema.parse({
@@ -505,6 +562,14 @@ describe('InsomniaFileSchema (discriminated union)', () => {
expect(() => InsomniaFileSchema.parse({ type: 'futureCollection.insomnia.rest/5.0' })).toThrow();
});
it('accepts mcp request', () => {
const mcpRequest = InsomniaFileSchema.parse({
type: 'mcpClient.insomnia/5.0',
mcpRequest: makeMcpRequest(),
});
expect(mcpRequest.type).toBe('mcpClient.insomnia/5.0');
});
it('rejects unknown type', () => {
expect(() => InsomniaFileSchema.parse({ type: 'nope' })).toThrow();
});

View File

@@ -10,6 +10,7 @@ import YAML from 'yaml';
import { INSOMNIA_SCHEMA_VERSION } from '../../common/insomnia-schema-migrations/schema-version';
import * as models from '../../models';
import { EnvironmentKvPairDataType } from '../../models/environment';
import type { Request } from '../../models/request';
import { database as db } from '../database';
import {
@@ -47,6 +48,7 @@ describe('Insomnia v5 Import/Export - Comprehensive Tests', () => {
expect(insomniaSchemaTypeToScope('environment.insomnia.rest/5.0')).toBe('environment');
expect(insomniaSchemaTypeToScope('spec.insomnia.rest/5.0')).toBe('design');
expect(insomniaSchemaTypeToScope('mock.insomnia.rest/5.0')).toBe('mock-server');
expect(insomniaSchemaTypeToScope('mcpClient.insomnia/5.0')).toBe('mcp');
});
});
@@ -324,6 +326,76 @@ collection: []
expect(parsed.server.url).toBe('http://localhost:3000');
});
it('handles mcp client scope', async () => {
const workspace = await models.workspace.create({
_id: 'wrk_mcp',
name: 'MCP Workspace',
parentId: 'proj_test',
scope: 'mcp',
});
await models.environment.create({
_id: 'env_mcp',
name: 'Base Env',
parentId: workspace._id,
data: {},
});
const mcpRequest = await models.mcpRequest.create({
_id: 'mcp-request_test',
name: 'Test MCP client',
parentId: workspace._id,
url: 'http://mcp.test.com/mcp',
transportType: 'streamable-http',
});
let result = await getInsomniaV5DataExport({
workspaceId: workspace._id,
includePrivateEnvironments: false,
});
let parsed = YAML.parse(result);
expect(parsed.type).toBe('mcpClient.insomnia/5.0');
expect(parsed.mcpRequest.url).toBe('http://mcp.test.com/mcp');
expect(parsed.mcpRequest.transportType).toBe('streamable-http');
await models.mcpRequest.update(mcpRequest, {
transportType: 'stdio',
url: 'npx mcp-client stdio',
env: [
{
id: 'var1',
name: 'foo',
value: 'bar',
type: EnvironmentKvPairDataType.STRING,
},
{
id: 'var2',
name: 'foo1',
value: 'bar1',
type: EnvironmentKvPairDataType.STRING,
},
],
roots: [
{
uri: 'file:///path/to/root',
},
],
});
result = await getInsomniaV5DataExport({
workspaceId: workspace._id,
includePrivateEnvironments: false,
});
parsed = YAML.parse(result);
expect(parsed.type).toBe('mcpClient.insomnia/5.0');
expect(parsed.mcpRequest.url).toBe('npx mcp-client stdio');
expect(parsed.mcpRequest.transportType).toBe('stdio');
expect(parsed.mcpRequest.env).toHaveLength(2);
expect(parsed.mcpRequest.roots).toHaveLength(1);
});
it('returns empty string for unknown workspace', async () => {
const result = await getInsomniaV5DataExport({
workspaceId: 'missing',

View File

@@ -465,6 +465,34 @@ export const SocketIORequestSchema = z.object({
eventListeners: SocketIOEventListenerSchema.array().optional(),
});
export const McpRequestSchema = z.object({
name: z.string().optional().default(''),
url: z.string().optional().default(''),
transportType: z.enum(['stdio', 'streamable-http']).optional().default('streamable-http'),
headers: HeadersSchema.optional(),
authentication: AuthenticationSchema.optional(),
meta: MetaSchema.optional(),
env: z
.array(
z.object({
id: z.string(),
name: z.string().optional().default(''),
value: z.string().optional().default(''),
type: z.literal('str'),
enabled: z.boolean().optional().default(true),
}),
)
.optional(),
roots: z
.array(
z.object({
name: z.string().optional(),
uri: z.string().optional().default(''),
}),
)
.optional(),
});
type Request = z.infer<typeof RequestSchema>;
type GRPCRequest = z.infer<typeof GRPCRequestSchema>;
type WebsocketRequest = z.infer<typeof WebsocketRequestSchema>;
@@ -578,12 +606,24 @@ const GlobalEnvironmentsSchema = z.object({
environments: EnvironmentSchema.optional(),
});
export const McpClientSchema = z.object({
// Does not follow the insomnia.rest pattern to prevent crashes in older versions when syncing this file: INS-1762
type: z.literal('mcpClient.insomnia/5.0'),
schema_version: z.string().optional().default(INSOMNIA_SCHEMA_VERSION),
name: z.string().optional(),
meta: MetaSchema.optional(),
mcpRequest: McpRequestSchema.optional(),
environments: EnvironmentSchema.optional(),
});
export const InsomniaFileSchema = z.discriminatedUnion('type', [
CollectionSchema,
ApiSpecSchema,
MockServerSchema,
GlobalEnvironmentsSchema,
McpClientSchema,
]);
export const InsomniaFileTypeValues = InsomniaFileSchema.options.map(option => option.shape.type.value);
export type InsomniaFile = z.infer<typeof InsomniaFileSchema>;

View File

@@ -1,6 +1,7 @@
import { z, type ZodError } from 'zod/v4';
import { insecureReadFile } from '~/main/secure-read-file';
import { isMcpRequest, type McpRequest } from '~/models/mcp-request';
import { type InsomniaImporter } from '../main/importers/convert';
import type { ImportEntry } from '../main/importers/entities';
@@ -27,6 +28,7 @@ import { generateId } from './misc';
export type AllExportTypes =
| 'request'
| 'mcp_request'
| 'grpc_request'
| 'websocket_request'
| 'websocket_payload'
@@ -117,6 +119,7 @@ export interface ScanResult {
unitTests?: UnitTest[];
unitTestSuites?: UnitTestSuite[];
mockRoutes?: MockRoute[];
mcpRequests?: McpRequest[];
type?: InsomniaImporter;
oriFileName?: string;
errors: string[];
@@ -133,6 +136,7 @@ let resourceCacheList: ResourceCacheType[] = [];
// All models that can be exported should be listed here
export const MODELS_BY_EXPORT_TYPE: Record<AllExportTypes, AllTypes> = {
request: 'Request',
mcp_request: 'McpRequest',
websocket_payload: 'WebSocketPayload',
websocket_request: 'WebSocketRequest',
socketio_payload: 'SocketIOPayload',
@@ -233,6 +237,7 @@ export async function scanResources(importEntries: ImportEntry[]): Promise<ScanR
const workspaces = resources.filter(isWorkspace);
const cookieJars = resources.filter(isCookieJar);
const mockRoutes = resources.filter(isMockRoute);
const mcpRequests = resources.filter(isMcpRequest);
return {
type,
@@ -244,6 +249,7 @@ export async function scanResources(importEntries: ImportEntry[]): Promise<ScanR
apiSpecs,
cookieJars,
mockRoutes,
mcpRequests,
oriFileName,
errors: [],
};

View File

@@ -17,11 +17,13 @@ import { parse, stringify } from 'yaml';
import { type AllExportTypes, MODELS_BY_EXPORT_TYPE } from '~/common/import';
import { migrateToLatestYaml } from '~/common/insomnia-schema-migrations';
import { INSOMNIA_SCHEMA_VERSION } from '~/common/insomnia-schema-migrations/schema-version';
import type { McpRequest } from '~/models/mcp-request';
import { invariant } from '~/utils/invariant';
import * as models from '../models';
import type { ApiSpec } from '../models/api-spec';
import type { CookieJar } from '../models/cookie-jar';
import type { EnvironmentKvPairData } from '../models/environment';
import { type Environment, maskVaultEnvironmentData } from '../models/environment';
import type { GrpcRequest } from '../models/grpc-request';
import type { MockRoute } from '../models/mock-route';
@@ -42,6 +44,7 @@ import {
type Insomnia_WebsocketRequest,
type InsomniaFile,
InsomniaFileSchema,
McpRequestSchema,
type Meta,
SocketIORequestSchema,
WebsocketRequestSchema,
@@ -279,6 +282,8 @@ export function insomniaSchemaTypeToScope(type: InsomniaFile['type']): Workspace
return 'environment';
} else if (type === 'spec.insomnia.rest/5.0') {
return 'design';
} else if (type === 'mcpClient.insomnia/5.0') {
return 'mcp';
}
return 'mock-server';
}
@@ -642,6 +647,53 @@ function getCollection(
return [];
}
function getMcpRequest(file: InsomniaFile): WithExportType<McpRequest>[] {
const commonProps: WithExportType<McpRequest> = {
...mapMetaToInsomniaMeta({
id: '__MCP_CLIENT_ID__',
}),
parentId: file.meta?.id || '__WORKSPACE_ID__',
name: file.name || 'MCP Client',
type: 'McpRequest',
_type: 'mcp_request',
url: '',
transportType: 'streamable-http',
description: '',
authentication: {},
headers: [],
env: [],
connected: false,
mcpStdioAccess: false,
roots: [],
subscribeResources: [],
sslValidation: true,
};
if ('mcpRequest' in file && file.mcpRequest) {
const mcpRequestParser = McpRequestSchema.safeParse(file.mcpRequest);
if (mcpRequestParser.success) {
const data = mcpRequestParser.data;
return [
{
...commonProps,
...mapMetaToInsomniaMeta(
data.meta || {
id: '__MCP_CLIENT_ID__',
},
),
url: data.url,
transportType: data.transportType,
authentication: data.authentication || {},
headers: mapHeaders(data.headers),
env: (data.env as EnvironmentKvPairData[]) || [],
},
];
}
}
return [commonProps];
}
function importData(rawData: string) {
// Apply schema migration before parsing to handle older schema versions
const migratedData = migrateToLatestYaml(rawData);
@@ -671,7 +723,10 @@ function importData(rawData: string) {
if (file.type === 'mock.insomnia.rest/5.0') {
return [getWorkspace(file), getMockServer(file), ...getMockRoutes(file)];
}
// @ts-expect-error this errors happen when new types are added but not handled here
if (file.type === 'mcpClient.insomnia/5.0') {
return [getWorkspace(file), ...getEnvironments(file), ...getMcpRequest(file)];
}
// @ts-expect-error: Exhaustiveness check
throw new Error(`No import handler found for type ${file.type}`);
}
throw new Error(`Failed to parse yaml file to Insomnia schema ${fileSchemaParser.error?.toString()}`);
@@ -1051,6 +1106,33 @@ export async function getInsomniaV5DataExport({
}));
}
function getMcpRequestFromResources(
resource: McpRequest,
): Extract<InsomniaFile, { type: 'mcpClient.insomnia/5.0' }>['mcpRequest'] {
return {
name: resource.name,
url: resource.url,
transportType: resource.transportType,
headers: resource.headers,
authentication: resource.authentication,
meta: {
id: resource._id,
created: resource.created,
modified: resource.modified,
},
env: resource.env.map(envVar => ({
id: envVar.id,
name: envVar.name,
value: envVar.value,
type: 'str',
enabled: !!envVar.enabled,
})),
roots: resource.roots.map(root => ({
uri: root.uri,
})),
};
}
if (workspace.scope === 'collection') {
const collection: InsomniaFile = {
type: 'collection.insomnia.rest/5.0',
@@ -1148,6 +1230,25 @@ export async function getInsomniaV5DataExport({
const parsedMockServer = InsomniaFileSchema.parse(mockServer);
return stringify(removeEmptyFields(parsedMockServer), {});
} else if (workspace.scope === 'mcp') {
const mcpRequest = exportableResources.find(models.mcpRequest.isMcpRequest);
invariant(mcpRequest, 'No MCP Request found in MCP workspace');
const mcpClient: InsomniaFile = {
type: 'mcpClient.insomnia/5.0',
schema_version: INSOMNIA_SCHEMA_VERSION,
name: workspace.name,
meta: mapWorkspaceMeta(workspace),
// each mcp workspace has exactly one mcpRequest
mcpRequest: getMcpRequestFromResources(mcpRequest),
environments: getEnvironmentsFromResources(
exportableResources.filter(models.environment.isEnvironment),
includePrivateEnvironments,
),
};
const parsedMcpClient = InsomniaFileSchema.parse(mcpClient);
return stringify(removeEmptyFields(parsedMcpClient));
}
throw new Error('Unknown workspace scope');
} catch (err) {

View File

@@ -38,7 +38,7 @@ import {
PLAYWRIGHT,
} from '../common/constants';
import { database } from '../common/database';
import { InsomniaFileSchema } from '../common/import-v5-parser';
import { InsomniaFileSchema, InsomniaFileTypeValues } from '../common/import-v5-parser';
import { migrateToLatestYaml } from '../common/insomnia-schema-migrations';
import { insomniaSchemaTypeToScope } from '../common/insomnia-v5';
import * as models from '../models';
@@ -641,7 +641,12 @@ async function isInsomniaFile(fullPath: string, fsClient: PromiseFsClient) {
}
const fileContents = await fsClient.promises.readFile(fullPath, 'utf8');
return fileContents.split('\n')[0].trim().includes('insomnia.rest');
const fileTypeStr = fileContents.split('\n')[0].trim();
const doesFileContainInsomniaV5FormatTypeString = InsomniaFileTypeValues.some(fileType =>
fileTypeStr.includes(fileType),
);
return doesFileContainInsomniaV5FormatTypeString;
}
// Recursively finds all .yaml files in a repository that are Insomnia files and returns their paths relative to the repo root.

View File

@@ -7,7 +7,7 @@ import type { VCS } from '../../sync/vcs/vcs';
import { insomniaFetch } from '../../ui/insomnia-fetch';
import { invariant } from '../../utils/invariant';
import { isDefaultOrganizationProject, type Project, update as updateProject } from '../project';
import { isMcp, type Workspace } from '../workspace';
import { type Workspace } from '../workspace';
import { getOrCreateByParentId as getOrCreateWorkspaceMeta } from '../workspace-meta';
export const sortProjects = (projects: Project[]) => [
...projects.filter(p => isDefaultOrganizationProject(p)).sort((a, b) => a.name.localeCompare(b.name)),
@@ -62,9 +62,6 @@ export async function updateLocalProjectToRemote({
});
for (const workspace of projectWorkspaces) {
if (isMcp(workspace)) {
continue;
}
const workspaceMeta = await getOrCreateWorkspaceMeta(workspace._id);
// Initialize Sync on the workspace if it's not using Git sync

View File

@@ -11,7 +11,7 @@ export const name = 'MCP Request';
export const type = 'McpRequest';
export const prefix = 'mcp-req';
export const canDuplicate = true;
export const canSync = false;
export const canSync = true;
export const TRANSPORT_TYPES = {
STDIO: 'stdio',
@@ -20,7 +20,6 @@ export const TRANSPORT_TYPES = {
export type TransportType = (typeof TRANSPORT_TYPES)[keyof typeof TRANSPORT_TYPES];
export interface BaseMcpRequest {
name: string;
url: string;
transportType: TransportType;
description: string;
@@ -48,7 +47,6 @@ export function init(): BaseMcpRequest {
return {
url: '',
transportType: TRANSPORT_TYPES.HTTP,
name: 'New MCP Client',
description: '',
headers: [],
authentication: {},

View File

@@ -79,8 +79,8 @@ export async function all() {
return await db.find<Workspace>(type);
}
export function count(scope?: WorkspaceScope) {
return db.count<Workspace>(type, scope ? { scope } : {});
export function count() {
return db.count(type);
}
export function update(workspace: Workspace, patch: Partial<Workspace>) {

View File

@@ -157,14 +157,12 @@ export const useRootLoaderData = () => {
export async function clientLoader(_args: Route.ClientLoaderArgs) {
const settings = await models.settings.get();
const workspaceCount = await models.workspace.count();
const mcpWorkspaceCount = await models.workspace.count('mcp');
const userSession = await models.userSession.getOrCreate();
const cloudCredentials = await models.cloudCredential.all();
return {
settings,
workspaceCount,
mcpWorkspaceCount,
userSession,
cloudCredentials,
};

View File

@@ -21,7 +21,7 @@ import type { RequestGroupMeta } from '~/models/request-group-meta';
import type { RequestMeta } from '~/models/request-meta';
import type { SocketIORequest } from '~/models/socket-io-request';
import type { WebSocketRequest } from '~/models/websocket-request';
import { isMcp, type Workspace } from '~/models/workspace';
import { type Workspace } from '~/models/workspace';
import type { WorkspaceMeta } from '~/models/workspace-meta';
import { pushSnapshotOnInitialize } from '~/sync/vcs/initialize-backend-project';
import { VCSInstance } from '~/sync/vcs/insomnia-sync';
@@ -257,8 +257,7 @@ export async function clientLoader({ params, request }: Route.ClientLoaderArgs)
const userSession = await models.userSession.getOrCreate();
const isLoggedInIsCloudProjectAndIsNotGitRepo = userSession.id && activeProject.remoteId && !gitRepository;
let vcsVersion = null;
// Mcp workspace do not support cloud sync for now
if (isLoggedInIsCloudProjectAndIsNotGitRepo && !isMcp(activeWorkspace)) {
if (isLoggedInIsCloudProjectAndIsNotGitRepo) {
try {
const vcs = VCSInstance();
await vcs.switchAndCreateBackendProjectIfNotExist(workspaceId, activeWorkspace.name);

View File

@@ -2,7 +2,7 @@ import { href, redirect } from 'react-router';
import * as models from '~/models';
import { isRemoteProject, type Project } from '~/models/project';
import { isMcp, type Workspace } from '~/models/workspace';
import { type Workspace } from '~/models/workspace';
import { VCSInstance } from '~/sync/vcs/insomnia-sync';
import { SegmentEvent } from '~/ui/analytics';
import { invariant } from '~/utils/invariant';
@@ -14,7 +14,7 @@ async function deleteWorkspaceFromCloud(workspace: Workspace, project: Project)
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
const isGitSync = !!workspaceMeta.gitRepositoryId;
if (isRemoteProject(project) && !isGitSync && !isMcp(workspace)) {
if (isRemoteProject(project) && !isGitSync) {
try {
const vcs = VCSInstance();
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);

View File

@@ -146,7 +146,7 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
await database.flushChanges(flushId);
const { id } = await models.userSession.getOrCreate();
if (id && !workspaceMeta.gitRepositoryId && !isGitProject(project) && !isLocalProject(project) && scope !== 'mcp') {
if (id && !workspaceMeta.gitRepositoryId && !isGitProject(project) && !isLocalProject(project)) {
const vcs = VCSInstance();
await initializeLocalBackendProjectAndMarkForSync({
vcs,
@@ -220,7 +220,7 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
parentId: workspace._id,
transportType: 'streamable-http',
url: '',
name: 'My first MCP Client',
name: 'MCP Client',
headers: defaultHeaders,
description: '',
});

View File

@@ -5,10 +5,10 @@ import YAML from 'yaml';
import { database, database as db } from '../../common/database';
import { extractErrorMessages } from '../../common/import';
import type { InsomniaFile } from '../../common/import-v5-parser';
import { type InsomniaFile, InsomniaFileTypeValues } from '../../common/import-v5-parser';
import { getInsomniaV5DataExport, tryImportV5Data } from '../../common/insomnia-v5';
import * as models from '../../models';
import { isMcp, isWorkspace, type Workspace } from '../../models/workspace';
import { isWorkspace, type Workspace } from '../../models/workspace';
import type { WorkspaceMeta } from '../../models/workspace-meta';
import Stat from './stat';
import { SystemError } from './system-error';
@@ -73,7 +73,10 @@ export class GitProjectNeDBClient {
const dataStr = data.toString();
const doesFileContainInsomniaV5FormatTypeString = dataStr.split('\n')[0].trim().includes('insomnia.rest');
const fileTypeStr = dataStr.split('\n')[0].trim();
const doesFileContainInsomniaV5FormatTypeString = InsomniaFileTypeValues.some(fileType =>
fileTypeStr.includes(fileType),
);
if (!doesFileContainInsomniaV5FormatTypeString) {
throw this._errMissing(filePath);
@@ -146,10 +149,8 @@ export class GitProjectNeDBClient {
async readdir(filePath: string) {
filePath = path.normalize(filePath);
// Exclude the mcp workspace since it's not supported in git sync
const workspaces = (await db.find<Workspace>(models.workspace.type, { parentId: this._projectId })).filter(
w => !isMcp(w),
);
const workspaces = await db.find<Workspace>(models.workspace.type, { parentId: this._projectId });
const workspaceMetas = await db.find<WorkspaceMeta>(models.workspaceMeta.type, {
parentId: {
$in: workspaces.map(w => w._id),

View File

@@ -38,6 +38,7 @@ import {
} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { McpActionsDropdown } from '~/ui/components/dropdowns/mcp-actions-dropdown';
import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown';
import { WorkspaceSyncDropdown } from '~/ui/components/dropdowns/workspace-sync-dropdown';
import { EnvironmentPicker } from '~/ui/components/environment-picker';
import { ErrorBoundary } from '~/ui/components/error-boundary';
import { Icon } from '~/ui/components/icon';
@@ -537,6 +538,8 @@ export const McpPane = () => {
</GridList>
</div>
</div>
<WorkspaceSyncDropdown />
{isEnvironmentModalOpen && <WorkspaceEnvironmentsEditModal onClose={() => setEnvironmentModalOpen(false)} />}
{isCertificatesModalOpen && <MCPCertificatesModal onClose={() => setCertificatesModalOpen(false)} />}
</div>

View File

@@ -28,7 +28,7 @@ import { useAIFeatureStatus } from '~/ui/hooks/use-organization-features';
import { type ApiSpec } from '../../../models/api-spec';
import { isGitProject, type Project } from '../../../models/project';
import { isMcp, type WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { type WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { safeToUseInsomniaFileName, safeToUseInsomniaFileNameWithExt } from '../../../sync/git/insomnia-filename';
import { SegmentEvent } from '../../analytics';
import { Icon } from '../icon';
@@ -72,12 +72,8 @@ export const NewWorkspaceModal = ({
const isEnterprise = currentPlan?.type.includes('enterprise');
const isSelfHostedDisabled = !storageRules.enableLocalVault;
const isCloudProjectDisabled = isLocalProject || !storageRules.enableCloudSync;
const isMcpWorkspace = isMcp({ scope });
// Mcp workspaces do not support Git sync for now
const isGitProjectAndNotMcpWorkspace = isGitProject(project) && !isMcpWorkspace;
const canOnlyCreateSelfHosted = isLocalProject && isEnterprise;
const defaultFileName = safeToUseInsomniaFileName(defaultNameByScope[scope]);
const { isGenerateMockServersWithAIEnabled } = useAIFeatureStatus();
@@ -99,8 +95,7 @@ export const NewWorkspaceModal = ({
name: defaultNameByScope[scope],
scope,
folderPath: '',
// Add a unique timestamp for mcp file name to avoid conflicts since we hide the Git file and folder selector for it.
fileName: isGitProject(project) && isMcpWorkspace ? `${defaultFileName}_${Date.now()}` : defaultFileName,
fileName: safeToUseInsomniaFileName(defaultNameByScope[scope]),
mockServerType: canOnlyCreateSelfHosted ? 'self-hosted' : 'cloud',
mockServerUrl: '',
mockServerCreationType: sourceApiSpec?.contents ? 'ai' : 'manual',
@@ -178,7 +173,7 @@ export const NewWorkspaceModal = ({
className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30"
>
<Modal
className={`flex max-h-[90dvh] w-full max-w-3xl flex-col overflow-hidden rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) text-(--color-font) ${isGitProjectAndNotMcpWorkspace ? 'min-h-[420px]' : 'min-h-[220px]'}`}
className={`flex max-h-[90dvh] w-full max-w-3xl flex-col overflow-hidden rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) text-(--color-font) ${isGitProject(project) ? 'min-h-[420px]' : 'min-h-[220px]'}`}
>
<Dialog
aria-label="Create or update dialog"
@@ -236,8 +231,7 @@ export const NewWorkspaceModal = ({
/>
<FieldError className="text-xs text-red-500" />
</TextField>
{/* Mcp workspaces do not support Git sync for now */}
{isGitProjectAndNotMcpWorkspace && (
{isGitProject(project) && (
<>
<TextField
name="fileName"

View File

@@ -12,7 +12,7 @@ import * as models from 'insomnia/src/models/index';
import { type BaseModel, environment } from 'insomnia/src/models/index';
import { isScratchpadOrganizationId, type Organization } from 'insomnia/src/models/organization';
import type { Project } from 'insomnia/src/models/project';
import { isMcp, isScratchpad, type Workspace } from 'insomnia/src/models/workspace';
import { isScratchpad, type Workspace } from 'insomnia/src/models/workspace';
import { SegmentEvent } from 'insomnia/src/ui/analytics';
import { Icon } from 'insomnia/src/ui/components/icon';
import { showError, showModal } from 'insomnia/src/ui/components/modals';
@@ -391,10 +391,8 @@ export async function exportWorkspaceData({
export async function exportAllData({ dirPath }: { dirPath: string }): Promise<void> {
const workspaces = await database.find<Workspace>(models.workspace.type);
const workspacesWithoutMcp = workspaces.filter(w => !isMcp(w));
const baseEnvironments = await database.find<Environment>(environment.type, {
parentId: { $in: workspacesWithoutMcp.map(w => w._id) },
parentId: { $in: workspaces.map(w => w._id) },
});
const subEnvironments = await database.find<Environment>(environment.type, {
@@ -408,7 +406,7 @@ export async function exportAllData({ dirPath }: { dirPath: string }): Promise<v
const insomniaExportFolder = window.path.join(dirPath, `insomnia-export.${Date.now()}`);
for (const workspace of workspacesWithoutMcp) {
for (const workspace of workspaces) {
await exportWorkspaceData({
workspace,
dirPath: insomniaExportFolder,
@@ -635,7 +633,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
const workspaceData = useWorkspaceLoaderData();
const activeWorkspaceName = workspaceData?.activeWorkspace.name;
const { workspaceCount, userSession, mcpWorkspaceCount } = useRootLoaderData()!;
const { workspaceCount, userSession } = useRootLoaderData()!;
const workspacesFetcher = useProjectListWorkspacesLoaderFetcher();
useEffect(() => {
const isIdleAndUninitialized = workspacesFetcher.state === 'idle' && !workspacesFetcher.data;
@@ -647,11 +645,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
}
}, [organizationId, projectId, workspacesFetcher]);
const projectLoaderData = workspacesFetcher?.data;
const workspacesForActiveProject =
projectLoaderData?.files
.map(w => w.workspace)
.filter(isNotNullOrUndefined)
.filter(w => !isMcp(w)) || [];
const workspacesForActiveProject = projectLoaderData?.files.map(w => w.workspace).filter(isNotNullOrUndefined) || [];
const activeProject = projectLoaderData?.activeProject;
const projectName = activeProject?.name ?? getProductName();
const projects = projectLoaderData?.projects || [];
@@ -717,7 +711,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
aria-label="Export all data"
>
<Icon icon="file-export" />
<span>Export all data {`(${workspaceCount - mcpWorkspaceCount} files)`}</span>
<span>Export all data {`(${workspaceCount} files)`}</span>
</Button>
);
}
@@ -785,7 +779,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
aria-label="Export all data"
>
<Icon icon="file-export" />
<span>Export all data {`(${workspaceCount - mcpWorkspaceCount} files)`}</span>
<span>Export all data {`(${workspaceCount} files)`}</span>
</Button>
<Button

View File

@@ -4,6 +4,7 @@ import * as models from '~/models';
import type { ApiSpec } from '~/models/api-spec';
import type { Environment } from '~/models/environment';
import type { GrpcRequest } from '~/models/grpc-request';
import type { McpRequest } from '~/models/mcp-request';
import type { MockRoute } from '~/models/mock-route';
import type { MockServer } from '~/models/mock-server';
import type { Request } from '~/models/request';
@@ -49,6 +50,7 @@ export async function getSyncItems({ workspaceId }: { workspaceId: string }) {
| Environment
| ApiSpec
| Request
| McpRequest
| WebSocketRequest
| SocketIORequest
| GrpcRequest
@@ -108,6 +110,11 @@ export async function getSyncItems({ workspaceId }: { workspaceId: string }) {
mockRoutes.map(m => syncItemsList.push(m));
}
const mcpRequest = await models.mcpRequest.getByParentId(workspaceId);
if (mcpRequest) {
syncItemsList.push(mcpRequest);
}
const baseEnvironment = await models.environment.getByParentId(workspaceId);
invariant(baseEnvironment, 'Base environment not found');