From e2fb1bcbbdddc23d6d3e63c3287c7d089940b6cc Mon Sep 17 00:00:00 2001 From: Kent Wang Date: Sat, 22 Nov 2025 20:36:22 +0800 Subject: [PATCH] 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 --- .../common/__tests__/import-v5-parser.test.ts | 65 +++++++++++ .../src/common/__tests__/insomnia-v5.test.ts | 72 ++++++++++++ .../insomnia/src/common/import-v5-parser.ts | 40 +++++++ packages/insomnia/src/common/import.ts | 6 + packages/insomnia/src/common/insomnia-v5.ts | 103 +++++++++++++++++- packages/insomnia/src/main/git-service.ts | 9 +- .../insomnia/src/models/helpers/project.ts | 5 +- packages/insomnia/src/models/mcp-request.ts | 4 +- packages/insomnia/src/models/workspace.ts | 4 +- packages/insomnia/src/root.tsx | 2 - ...ject.$projectId.workspace.$workspaceId.tsx | 5 +- ...Id.project.$projectId.workspace.delete.tsx | 4 +- ...ionId.project.$projectId.workspace.new.tsx | 4 +- .../src/sync/git/project-ne-db-client.ts | 15 +-- .../src/ui/components/mcp/mcp-pane.tsx | 3 + .../components/modals/new-workspace-modal.tsx | 14 +-- .../ui/components/settings/import-export.tsx | 20 ++-- packages/insomnia/src/ui/sync-utils.ts | 7 ++ 18 files changed, 331 insertions(+), 51 deletions(-) diff --git a/packages/insomnia/src/common/__tests__/import-v5-parser.test.ts b/packages/insomnia/src/common/__tests__/import-v5-parser.test.ts index c288d22a29..52fa86e8a6 100644 --- a/packages/insomnia/src/common/__tests__/import-v5-parser.test.ts +++ b/packages/insomnia/src/common/__tests__/import-v5-parser.test.ts @@ -12,6 +12,8 @@ import { JsonSchema, KeyLiteralSchema, LiteralSchema, + McpClientSchema, + McpRequestSchema, MetaGroupSchema, MetaSchema, MockRouteSchema, @@ -131,6 +133,15 @@ const makeMockRoute = (overrides: Record = {}) => ({ ...overrides, }); +const makeMcpRequest = (overrides: Record = {}) => ({ + 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(); }); diff --git a/packages/insomnia/src/common/__tests__/insomnia-v5.test.ts b/packages/insomnia/src/common/__tests__/insomnia-v5.test.ts index 1532aff926..c7b36b0557 100644 --- a/packages/insomnia/src/common/__tests__/insomnia-v5.test.ts +++ b/packages/insomnia/src/common/__tests__/insomnia-v5.test.ts @@ -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', diff --git a/packages/insomnia/src/common/import-v5-parser.ts b/packages/insomnia/src/common/import-v5-parser.ts index bbf81fd19b..1eb4ec528b 100644 --- a/packages/insomnia/src/common/import-v5-parser.ts +++ b/packages/insomnia/src/common/import-v5-parser.ts @@ -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; type GRPCRequest = z.infer; type WebsocketRequest = z.infer; @@ -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; diff --git a/packages/insomnia/src/common/import.ts b/packages/insomnia/src/common/import.ts index 60f9d5d299..e5fd81ddfb 100644 --- a/packages/insomnia/src/common/import.ts +++ b/packages/insomnia/src/common/import.ts @@ -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 = { 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[] { + const commonProps: WithExportType = { + ...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['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) { diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts index 7145b181c7..01cf494f7e 100644 --- a/packages/insomnia/src/main/git-service.ts +++ b/packages/insomnia/src/main/git-service.ts @@ -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. diff --git a/packages/insomnia/src/models/helpers/project.ts b/packages/insomnia/src/models/helpers/project.ts index 912fc62b84..1670b11568 100644 --- a/packages/insomnia/src/models/helpers/project.ts +++ b/packages/insomnia/src/models/helpers/project.ts @@ -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 diff --git a/packages/insomnia/src/models/mcp-request.ts b/packages/insomnia/src/models/mcp-request.ts index 46e3597c05..20fb11dfcd 100644 --- a/packages/insomnia/src/models/mcp-request.ts +++ b/packages/insomnia/src/models/mcp-request.ts @@ -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: {}, diff --git a/packages/insomnia/src/models/workspace.ts b/packages/insomnia/src/models/workspace.ts index 6df2c5bcea..e1a93e9d75 100644 --- a/packages/insomnia/src/models/workspace.ts +++ b/packages/insomnia/src/models/workspace.ts @@ -79,8 +79,8 @@ export async function all() { return await db.find(type); } -export function count(scope?: WorkspaceScope) { - return db.count(type, scope ? { scope } : {}); +export function count() { + return db.count(type); } export function update(workspace: Workspace, patch: Partial) { diff --git a/packages/insomnia/src/root.tsx b/packages/insomnia/src/root.tsx index 70efc0cd65..ebe88908bf 100644 --- a/packages/insomnia/src/root.tsx +++ b/packages/insomnia/src/root.tsx @@ -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, }; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx index 86d3fef8f0..03a5995f24 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx @@ -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); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.delete.tsx index fd6f444722..6e9d22f711 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.delete.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.delete.tsx @@ -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); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx index 8aeeb43148..bfc2bdd6a6 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx @@ -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: '', }); diff --git a/packages/insomnia/src/sync/git/project-ne-db-client.ts b/packages/insomnia/src/sync/git/project-ne-db-client.ts index df9ca353e3..1ee6b75a2e 100644 --- a/packages/insomnia/src/sync/git/project-ne-db-client.ts +++ b/packages/insomnia/src/sync/git/project-ne-db-client.ts @@ -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(models.workspace.type, { parentId: this._projectId })).filter( - w => !isMcp(w), - ); + const workspaces = await db.find(models.workspace.type, { parentId: this._projectId }); + const workspaceMetas = await db.find(models.workspaceMeta.type, { parentId: { $in: workspaces.map(w => w._id), diff --git a/packages/insomnia/src/ui/components/mcp/mcp-pane.tsx b/packages/insomnia/src/ui/components/mcp/mcp-pane.tsx index 0fb83706e3..b5c681c09c 100644 --- a/packages/insomnia/src/ui/components/mcp/mcp-pane.tsx +++ b/packages/insomnia/src/ui/components/mcp/mcp-pane.tsx @@ -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 = () => { + + {isEnvironmentModalOpen && setEnvironmentModalOpen(false)} />} {isCertificatesModalOpen && setCertificatesModalOpen(false)} />} diff --git a/packages/insomnia/src/ui/components/modals/new-workspace-modal.tsx b/packages/insomnia/src/ui/components/modals/new-workspace-modal.tsx index eace9a3d41..8362582242 100644 --- a/packages/insomnia/src/ui/components/modals/new-workspace-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/new-workspace-modal.tsx @@ -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" > - {/* Mcp workspaces do not support Git sync for now */} - {isGitProjectAndNotMcpWorkspace && ( + {isGitProject(project) && ( <> { const workspaces = await database.find(models.workspace.type); - const workspacesWithoutMcp = workspaces.filter(w => !isMcp(w)); - const baseEnvironments = await database.find(environment.type, { - parentId: { $in: workspacesWithoutMcp.map(w => w._id) }, + parentId: { $in: workspaces.map(w => w._id) }, }); const subEnvironments = await database.find(environment.type, { @@ -408,7 +406,7 @@ export async function exportAllData({ dirPath }: { dirPath: string }): Promise = ({ 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 = ({ 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 = ({ hideSettingsModal, onModalChange }) => aria-label="Export all data" > - Export all data {`(${workspaceCount - mcpWorkspaceCount} files)`} + Export all data {`(${workspaceCount} files)`} ); } @@ -785,7 +779,7 @@ export const ImportExport: FC = ({ hideSettingsModal, onModalChange }) => aria-label="Export all data" > - Export all data {`(${workspaceCount - mcpWorkspaceCount} files)`} + Export all data {`(${workspaceCount} files)`}