v0.5.47: deploy workflow as mcp, kb chunks tokenizer, UI improvements, jira service management tools
@@ -1,45 +1,35 @@
|
||||
---
|
||||
description: EMCN component library patterns with CVA
|
||||
description: EMCN component library patterns
|
||||
globs: ["apps/sim/components/emcn/**"]
|
||||
---
|
||||
|
||||
# EMCN Component Guidelines
|
||||
# EMCN Components
|
||||
|
||||
## When to Use CVA vs Direct Styles
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files).
|
||||
|
||||
**Use CVA (class-variance-authority) when:**
|
||||
- 2+ visual variants (primary, secondary, outline)
|
||||
- Multiple sizes or state variations
|
||||
- Example: Button with variants
|
||||
## CVA vs Direct Styles
|
||||
|
||||
**Use direct className when:**
|
||||
- Single consistent style
|
||||
- No variations needed
|
||||
- Example: Label with one style
|
||||
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
|
||||
|
||||
## Patterns
|
||||
|
||||
**With CVA:**
|
||||
```tsx
|
||||
const buttonVariants = cva('base-classes', {
|
||||
variants: {
|
||||
variant: { default: '...', primary: '...' },
|
||||
size: { sm: '...', md: '...' }
|
||||
}
|
||||
variants: { variant: { default: '...', primary: '...' } }
|
||||
})
|
||||
export { Button, buttonVariants }
|
||||
```
|
||||
|
||||
**Without CVA:**
|
||||
**Use direct className when:** Single consistent style, no variations
|
||||
|
||||
```tsx
|
||||
function Label({ className, ...props }) {
|
||||
return <Primitive className={cn('single-style-classes', className)} {...props} />
|
||||
return <Primitive className={cn('style-classes', className)} {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Use Radix UI primitives for accessibility
|
||||
- Export component and variants (if using CVA)
|
||||
- TSDoc with usage examples
|
||||
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
||||
- Always use `transition-colors` for hover states
|
||||
- `transition-colors` for hover states
|
||||
|
||||
@@ -8,7 +8,7 @@ alwaysApply: true
|
||||
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||
|
||||
## Logging
|
||||
Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
|
||||
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
|
||||
|
||||
## Comments
|
||||
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
||||
|
||||
@@ -10,58 +10,47 @@ globs: ["apps/sim/**"]
|
||||
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
|
||||
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
|
||||
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
|
||||
5. **Performance by Default**: useMemo, useCallback, refs appropriately
|
||||
|
||||
## File Organization
|
||||
## Root-Level Structure
|
||||
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
```
|
||||
|
||||
## Feature Organization
|
||||
|
||||
Features live under `app/workspace/[workspaceId]/`:
|
||||
|
||||
```
|
||||
feature/
|
||||
├── components/ # Feature components
|
||||
│ └── sub-feature/ # Sub-feature with own components
|
||||
├── hooks/ # Custom hooks
|
||||
└── feature.tsx # Main component
|
||||
├── components/ # Feature components
|
||||
├── hooks/ # Feature-scoped hooks
|
||||
├── utils/ # Feature-scoped utilities (2+ consumers)
|
||||
├── feature.tsx # Main component
|
||||
└── page.tsx # Next.js page entry
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
- **Components**: PascalCase (`WorkflowList`, `TriggerPanel`)
|
||||
- **Hooks**: camelCase with `use` prefix (`useWorkflowOperations`)
|
||||
- **Files**: kebab-case matching export (`workflow-list.tsx`)
|
||||
- **Stores**: kebab-case in stores/ (`sidebar/store.ts`)
|
||||
- **Components**: PascalCase (`WorkflowList`)
|
||||
- **Hooks**: `use` prefix (`useWorkflowOperations`)
|
||||
- **Files**: kebab-case (`workflow-list.tsx`)
|
||||
- **Stores**: `stores/feature/store.ts`
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
|
||||
|
||||
## State Management
|
||||
## Utils Rules
|
||||
|
||||
**useState**: UI-only concerns (dropdown open, hover, form inputs)
|
||||
**Zustand**: Shared state, persistence, global app state
|
||||
**useRef**: DOM refs, avoiding dependency issues, mutable non-reactive values
|
||||
|
||||
## Component Extraction
|
||||
|
||||
**Extract to separate file when:**
|
||||
- Complex (50+ lines)
|
||||
- Used across 2+ files
|
||||
- Has own state/logic
|
||||
|
||||
**Keep inline when:**
|
||||
- Simple (< 10 lines)
|
||||
- Used in only 1 file
|
||||
- Purely presentational
|
||||
|
||||
**Never import utilities from another component file.** Extract shared helpers to `lib/` or `utils/`.
|
||||
|
||||
## Utils Files
|
||||
|
||||
**Never create a `utils.ts` file for a single consumer.** Inline the logic directly in the consuming component.
|
||||
|
||||
**Create `utils.ts` when:**
|
||||
- 2+ files import the same helper
|
||||
|
||||
**Prefer existing sources of truth:**
|
||||
- Before duplicating logic, check if a centralized helper already exists (e.g., `lib/logs/get-trigger-options.ts`)
|
||||
- Import from the source of truth rather than creating wrapper functions
|
||||
|
||||
**Location hierarchy:**
|
||||
- `lib/` — App-wide utilities (auth, billing, core)
|
||||
- `feature/utils.ts` — Feature-scoped utilities (used by 2+ components in the feature)
|
||||
- Inline — Single-use helpers (define directly in the component)
|
||||
- **Never create `utils.ts` for single consumer** - inline it
|
||||
- **Create `utils.ts` when** 2+ files need the same helper
|
||||
- **Check existing sources** before duplicating (`lib/` has many utilities)
|
||||
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)
|
||||
|
||||
@@ -6,59 +6,43 @@ globs: ["apps/sim/**/*.tsx"]
|
||||
# Component Patterns
|
||||
|
||||
## Structure Order
|
||||
|
||||
```typescript
|
||||
'use client' // Only if using hooks
|
||||
|
||||
// 1. Imports (external → internal → relative)
|
||||
// 2. Constants at module level
|
||||
// Imports (external → internal)
|
||||
// Constants at module level
|
||||
const CONFIG = { SPACING: 8 } as const
|
||||
|
||||
// 3. Props interface with TSDoc
|
||||
// Props interface
|
||||
interface ComponentProps {
|
||||
/** Description */
|
||||
requiredProp: string
|
||||
optionalProp?: boolean
|
||||
}
|
||||
|
||||
// 4. Component with TSDoc
|
||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||
// a. Refs
|
||||
// b. External hooks (useParams, useRouter)
|
||||
// c. Store hooks
|
||||
// d. Custom hooks
|
||||
// e. Local state
|
||||
// f. useMemo computations
|
||||
// g. useCallback handlers
|
||||
// f. useMemo
|
||||
// g. useCallback
|
||||
// h. useEffect
|
||||
// i. Return JSX
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
1. Add `'use client'` when using React hooks
|
||||
|
||||
1. `'use client'` only when using React hooks
|
||||
2. Always define props interface
|
||||
3. TSDoc on component: description, @param, @returns
|
||||
4. Extract constants with `as const`
|
||||
5. Use Tailwind only, no inline styles
|
||||
6. Semantic HTML (`aside`, `nav`, `article`)
|
||||
7. Include ARIA attributes where appropriate
|
||||
8. Optional chain callbacks: `onAction?.(id)`
|
||||
3. Extract constants with `as const`
|
||||
4. Semantic HTML (`aside`, `nav`, `article`)
|
||||
5. Optional chain callbacks: `onAction?.(id)`
|
||||
|
||||
## Factory Pattern with Caching
|
||||
## Component Extraction
|
||||
|
||||
When generating components for a specific signature (e.g., icons):
|
||||
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
|
||||
|
||||
```typescript
|
||||
const cache = new Map<string, React.ComponentType<{ className?: string }>>()
|
||||
|
||||
function getColorIcon(color: string) {
|
||||
if (cache.has(color)) return cache.get(color)!
|
||||
|
||||
const Icon = ({ className }: { className?: string }) => (
|
||||
<div className={cn(className, 'rounded-[3px]')} style={{ backgroundColor: color, width: 10, height: 10 }} />
|
||||
)
|
||||
Icon.displayName = `ColorIcon(${color})`
|
||||
cache.set(color, Icon)
|
||||
return Icon
|
||||
}
|
||||
```
|
||||
**Keep inline when:** < 10 lines, single use, purely presentational
|
||||
|
||||
@@ -6,21 +6,13 @@ globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"]
|
||||
# Hook Patterns
|
||||
|
||||
## Structure
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('useFeatureName')
|
||||
|
||||
interface UseFeatureProps {
|
||||
id: string
|
||||
onSuccess?: (result: Result) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook description.
|
||||
* @param props - Configuration
|
||||
* @returns State and operations
|
||||
*/
|
||||
export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
// 1. Refs for stable dependencies
|
||||
const idRef = useRef(id)
|
||||
@@ -29,7 +21,6 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
// 2. State
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
// 3. Sync refs
|
||||
useEffect(() => {
|
||||
@@ -37,32 +28,27 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||
onSuccessRef.current = onSuccess
|
||||
}, [id, onSuccess])
|
||||
|
||||
// 4. Operations with useCallback
|
||||
// 4. Operations (useCallback with empty deps when using refs)
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
|
||||
setData(result)
|
||||
onSuccessRef.current?.(result)
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
logger.error('Failed', { error: err })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, []) // Empty deps - using refs
|
||||
}, [])
|
||||
|
||||
// 5. Return grouped by state/operations
|
||||
return { data, isLoading, error, fetchData }
|
||||
return { data, isLoading, fetchData }
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. Single responsibility per hook
|
||||
2. Props interface required
|
||||
3. TSDoc required
|
||||
4. Use logger, not console.log
|
||||
5. Refs for stable callback dependencies
|
||||
6. Wrap returned functions in useCallback
|
||||
7. Always try/catch async operations
|
||||
8. Track loading/error states
|
||||
3. Refs for stable callback dependencies
|
||||
4. Wrap returned functions in useCallback
|
||||
5. Always try/catch async operations
|
||||
6. Track loading/error states
|
||||
|
||||
@@ -5,33 +5,45 @@ globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
|
||||
|
||||
# Import Patterns
|
||||
|
||||
## EMCN Components
|
||||
Import from `@/components/emcn`, never from subpaths like `@/components/emcn/components/modal/modal`.
|
||||
## Absolute Imports
|
||||
|
||||
**Exception**: CSS imports use actual file paths: `import '@/components/emcn/components/code/code.css'`
|
||||
**Always use absolute imports.** Never use relative imports.
|
||||
|
||||
## Feature Components
|
||||
Import from central folder indexes, not specific subfolders:
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
|
||||
// ✓ Good
|
||||
import { useWorkflowStore } from '@/stores/workflows/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
// ❌ Wrong
|
||||
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard'
|
||||
// ✗ Bad
|
||||
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||
```
|
||||
|
||||
## Internal vs External
|
||||
- **Cross-feature**: Absolute paths through central index
|
||||
- **Within feature**: Relative paths (`./components/...`, `../utils`)
|
||||
## Barrel Exports
|
||||
|
||||
Use barrel exports (`index.ts`) when a folder has 3+ exports. Import from barrel, not individual files.
|
||||
|
||||
```typescript
|
||||
// ✓ Good
|
||||
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
|
||||
|
||||
// ✗ Bad
|
||||
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
|
||||
```
|
||||
|
||||
## Import Order
|
||||
|
||||
1. React/core libraries
|
||||
2. External libraries
|
||||
3. UI components (`@/components/emcn`, `@/components/ui`)
|
||||
4. Utilities (`@/lib/...`)
|
||||
5. Feature imports from indexes
|
||||
6. Relative imports
|
||||
5. Stores (`@/stores/...`)
|
||||
6. Feature imports
|
||||
7. CSS imports
|
||||
|
||||
## Types
|
||||
Use `type` keyword: `import type { WorkflowLog } from '...'`
|
||||
## Type Imports
|
||||
|
||||
Use `type` keyword for type-only imports:
|
||||
|
||||
```typescript
|
||||
import type { WorkflowLog } from '@/stores/logs/types'
|
||||
```
|
||||
|
||||
207
.cursor/rules/sim-integrations.mdc
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
description: Adding new integrations (tools, blocks, triggers)
|
||||
globs: ["apps/sim/tools/**", "apps/sim/blocks/**", "apps/sim/triggers/**"]
|
||||
---
|
||||
|
||||
# Adding Integrations
|
||||
|
||||
## Overview
|
||||
|
||||
Adding a new integration typically requires:
|
||||
1. **Tools** - API operations (`tools/{service}/`)
|
||||
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
|
||||
3. **Icon** - SVG icon (`components/icons.tsx`)
|
||||
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
|
||||
|
||||
Always look up the service's API docs first.
|
||||
|
||||
## 1. Tools (`tools/{service}/`)
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Export all tools
|
||||
├── types.ts # Params/response types
|
||||
├── {action}.ts # Individual tool (e.g., send_message.ts)
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Tool file structure:**
|
||||
|
||||
```typescript
|
||||
// tools/{service}/{action}.ts
|
||||
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
|
||||
id: '{service}_{action}',
|
||||
name: '{Service} {Action}',
|
||||
description: 'What this tool does',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: '{service}' }, // if OAuth
|
||||
params: { /* param definitions */ },
|
||||
request: {
|
||||
url: '/api/tools/{service}/{action}',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({ ...params }),
|
||||
},
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!data.success) throw new Error(data.error)
|
||||
return { success: true, output: data.output }
|
||||
},
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
**Register in `tools/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}{Action}Tool } from '@/tools/{service}'
|
||||
// Add to registry object
|
||||
{service}_{action}: {service}{Action}Tool,
|
||||
```
|
||||
|
||||
## 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { {Service}Response } from '@/tools/{service}/types'
|
||||
|
||||
export const {Service}Block: BlockConfig<{Service}Response> = {
|
||||
type: '{service}',
|
||||
name: '{Service}',
|
||||
description: 'Short description',
|
||||
longDescription: 'Detailed description',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: {Service}Icon,
|
||||
subBlocks: [ /* see SubBlock Properties below */ ],
|
||||
tools: {
|
||||
access: ['{service}_{action}', ...],
|
||||
config: {
|
||||
tool: (params) => `{service}_${params.operation}`,
|
||||
params: (params) => ({ ...params }),
|
||||
},
|
||||
},
|
||||
inputs: { /* input definitions */ },
|
||||
outputs: { /* output definitions */ },
|
||||
}
|
||||
```
|
||||
|
||||
### SubBlock Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'fieldName', // Unique identifier
|
||||
title: 'Field Label', // UI label
|
||||
type: 'short-input', // See SubBlock Types below
|
||||
placeholder: 'Hint text',
|
||||
required: true, // See Required below
|
||||
condition: { ... }, // See Condition below
|
||||
dependsOn: ['otherField'], // See DependsOn below
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
|
||||
|
||||
### `condition` - Show/hide based on another field
|
||||
|
||||
```typescript
|
||||
// Show when operation === 'send'
|
||||
condition: { field: 'operation', value: 'send' }
|
||||
|
||||
// Show when operation is 'send' OR 'read'
|
||||
condition: { field: 'operation', value: ['send', 'read'] }
|
||||
|
||||
// Show when operation !== 'send'
|
||||
condition: { field: 'operation', value: 'send', not: true }
|
||||
|
||||
// Complex: NOT in list AND another condition
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_channels', 'list_users'],
|
||||
not: true,
|
||||
and: { field: 'destinationType', value: 'dm', not: true }
|
||||
}
|
||||
```
|
||||
|
||||
### `required` - Field validation
|
||||
|
||||
```typescript
|
||||
// Always required
|
||||
required: true
|
||||
|
||||
// Conditionally required (same syntax as condition)
|
||||
required: { field: 'operation', value: 'send' }
|
||||
```
|
||||
|
||||
### `dependsOn` - Clear field when dependencies change
|
||||
|
||||
```typescript
|
||||
// Clear when credential changes
|
||||
dependsOn: ['credential']
|
||||
|
||||
// Clear when authMethod changes AND (credential OR botToken) changes
|
||||
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
||||
```
|
||||
|
||||
### `mode` - When to show field
|
||||
|
||||
- `'basic'` - Only in basic mode (default UI)
|
||||
- `'advanced'` - Only in advanced mode (manual input)
|
||||
- `'both'` - Show in both modes (default)
|
||||
- `'trigger'` - Only when block is used as trigger
|
||||
|
||||
**Register in `blocks/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||
// Add to registry object (alphabetically)
|
||||
{service}: {Service}Block,
|
||||
```
|
||||
|
||||
## 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* SVG path from service's brand assets */}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Export all triggers
|
||||
├── webhook.ts # Webhook handler
|
||||
├── utils.ts # Shared utilities
|
||||
└── {event}.ts # Specific event handlers
|
||||
```
|
||||
|
||||
**Register in `triggers/registry.ts`:**
|
||||
|
||||
```typescript
|
||||
import { {service}WebhookTrigger } from '@/triggers/{service}'
|
||||
// Add to TRIGGER_REGISTRY
|
||||
{service}_webhook: {service}WebhookTrigger,
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Look up API docs for the service
|
||||
- [ ] Create `tools/{service}/types.ts` with proper types
|
||||
- [ ] Create tool files for each operation
|
||||
- [ ] Create `tools/{service}/index.ts` barrel export
|
||||
- [ ] Register tools in `tools/registry.ts`
|
||||
- [ ] Add icon to `components/icons.tsx`
|
||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||
- [ ] Register block in `blocks/registry.ts`
|
||||
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||
66
.cursor/rules/sim-queries.mdc
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
description: React Query patterns for the Sim application
|
||||
globs: ["apps/sim/hooks/queries/**/*.ts"]
|
||||
---
|
||||
|
||||
# React Query Patterns
|
||||
|
||||
All React Query hooks live in `hooks/queries/`.
|
||||
|
||||
## Query Key Factory
|
||||
|
||||
Every query file defines a keys factory:
|
||||
|
||||
```typescript
|
||||
export const entityKeys = {
|
||||
all: ['entity'] as const,
|
||||
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
|
||||
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] as const,
|
||||
}
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```typescript
|
||||
// 1. Query keys factory
|
||||
// 2. Types (if needed)
|
||||
// 3. Private fetch functions
|
||||
// 4. Exported hooks
|
||||
```
|
||||
|
||||
## Query Hook
|
||||
|
||||
```typescript
|
||||
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: entityKeys.list(workspaceId),
|
||||
queryFn: () => fetchEntities(workspaceId as string),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Mutation Hook
|
||||
|
||||
```typescript
|
||||
export function useCreateEntity() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (variables) => { /* fetch POST */ },
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Optimistic Updates
|
||||
|
||||
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
|
||||
|
||||
## Naming
|
||||
|
||||
- **Keys**: `entityKeys`
|
||||
- **Query hooks**: `useEntity`, `useEntityList`
|
||||
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
|
||||
- **Fetch functions**: `fetchEntity` (private)
|
||||
@@ -5,53 +5,66 @@ globs: ["apps/sim/**/store.ts", "apps/sim/**/stores/**/*.ts"]
|
||||
|
||||
# Zustand Store Patterns
|
||||
|
||||
## Structure
|
||||
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
|
||||
|
||||
## Basic Store
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import type { FeatureState } from '@/stores/feature/types'
|
||||
|
||||
const initialState = { items: [] as Item[], activeId: null as string | null }
|
||||
|
||||
export const useFeatureStore = create<FeatureState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
setItems: (items) => set({ items }),
|
||||
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
{ name: 'feature-store' }
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Persisted Store
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
interface FeatureState {
|
||||
// State
|
||||
items: Item[]
|
||||
activeId: string | null
|
||||
|
||||
// Actions
|
||||
setItems: (items: Item[]) => void
|
||||
addItem: (item: Item) => void
|
||||
clearState: () => void
|
||||
}
|
||||
|
||||
const createInitialState = () => ({
|
||||
items: [],
|
||||
activeId: null,
|
||||
})
|
||||
|
||||
export const useFeatureStore = create<FeatureState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...createInitialState(),
|
||||
|
||||
setItems: (items) => set({ items }),
|
||||
|
||||
addItem: (item) => set((state) => ({
|
||||
items: [...state.items, item],
|
||||
})),
|
||||
|
||||
clearState: () => set(createInitialState()),
|
||||
width: 300,
|
||||
setWidth: (width) => set({ width }),
|
||||
_hasHydrated: false,
|
||||
setHasHydrated: (v) => set({ _hasHydrated: v }),
|
||||
}),
|
||||
{
|
||||
name: 'feature-state',
|
||||
partialize: (state) => ({ items: state.items }),
|
||||
partialize: (state) => ({ width: state.width }),
|
||||
onRehydrateStorage: () => (state) => state?.setHasHydrated(true),
|
||||
}
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Rules
|
||||
1. Interface includes state and actions
|
||||
2. Extract config to module constants
|
||||
3. TSDoc on store
|
||||
4. Only persist what's needed
|
||||
5. Immutable updates only - never mutate
|
||||
6. Use `set((state) => ...)` when depending on previous state
|
||||
7. Provide clear/reset actions
|
||||
|
||||
1. Use `devtools` middleware (named stores)
|
||||
2. Use `persist` only when data should survive reload
|
||||
3. `partialize` to persist only necessary state
|
||||
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
|
||||
5. Immutable updates only
|
||||
6. `set((state) => ...)` when depending on previous state
|
||||
7. Provide `reset()` action
|
||||
|
||||
## Outside React
|
||||
|
||||
```typescript
|
||||
const items = useFeatureStore.getState().items
|
||||
useFeatureStore.setState({ items: newItems })
|
||||
```
|
||||
|
||||
@@ -6,13 +6,14 @@ globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
|
||||
# Styling Rules
|
||||
|
||||
## Tailwind
|
||||
1. **No inline styles** - Use Tailwind classes exclusively
|
||||
2. **No duplicate dark classes** - Don't add `dark:` when value matches light mode
|
||||
3. **Exact values** - Use design system values (`text-[14px]`, `h-[25px]`)
|
||||
4. **Prefer px** - Use `px-[4px]` over `px-1`
|
||||
5. **Transitions** - Add `transition-colors` for interactive states
|
||||
|
||||
1. **No inline styles** - Use Tailwind classes
|
||||
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
|
||||
3. **Exact values** - `text-[14px]`, `h-[25px]`
|
||||
4. **Transitions** - `transition-colors` for interactive states
|
||||
|
||||
## Conditional Classes
|
||||
|
||||
```typescript
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -23,25 +24,17 @@ import { cn } from '@/lib/utils'
|
||||
)} />
|
||||
```
|
||||
|
||||
## CSS Variables for Dynamic Styles
|
||||
## CSS Variables
|
||||
|
||||
For dynamic values (widths, heights) synced with stores:
|
||||
|
||||
```typescript
|
||||
// In store setter
|
||||
setSidebarWidth: (width) => {
|
||||
set({ sidebarWidth: width })
|
||||
// In store
|
||||
setWidth: (width) => {
|
||||
set({ width })
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
|
||||
}
|
||||
|
||||
// In component
|
||||
<aside style={{ width: 'var(--sidebar-width)' }} />
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
```typescript
|
||||
// ❌ Bad
|
||||
<div style={{ width: 200 }}>
|
||||
<div className='text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
|
||||
// ✅ Good
|
||||
<div className='w-[200px]'>
|
||||
<div className='text-[var(--text-primary)]'>
|
||||
```
|
||||
|
||||
60
.cursor/rules/sim-testing.mdc
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Testing patterns with Vitest
|
||||
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
|
||||
---
|
||||
|
||||
# Testing Patterns
|
||||
|
||||
Use Vitest. Test files live next to source: `feature.ts` → `feature.test.ts`
|
||||
|
||||
## Structure
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Tests for [feature name]
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
// 1. Mocks BEFORE imports
|
||||
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
// 2. Imports AFTER mocks
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { createSession, loggerMock } from '@sim/testing'
|
||||
import { myFunction } from '@/lib/feature'
|
||||
|
||||
describe('myFunction', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('should do something', () => {
|
||||
expect(myFunction()).toBe(expected)
|
||||
})
|
||||
|
||||
it.concurrent('runs in parallel', () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
## @sim/testing Package
|
||||
|
||||
```typescript
|
||||
// Factories - create test data
|
||||
import { createBlock, createWorkflow, createSession } from '@sim/testing'
|
||||
|
||||
// Mocks - pre-configured mocks
|
||||
import { loggerMock, databaseMock, fetchMock } from '@sim/testing'
|
||||
|
||||
// Builders - fluent API for complex objects
|
||||
import { ExecutionBuilder, WorkflowBuilder } from '@sim/testing'
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. `@vitest-environment node` directive at file top
|
||||
2. **Mocks before imports** - `vi.mock()` calls must come first
|
||||
3. Use `@sim/testing` factories over manual test data
|
||||
4. `it.concurrent` for independent tests (faster)
|
||||
5. `beforeEach(() => vi.clearAllMocks())` to reset state
|
||||
6. Group related tests with nested `describe` blocks
|
||||
7. Test file naming: `*.test.ts` (not `*.spec.ts`)
|
||||
@@ -6,19 +6,15 @@ globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
|
||||
# TypeScript Rules
|
||||
|
||||
1. **No `any`** - Use proper types or `unknown` with type guards
|
||||
2. **Props interface** - Always define, even for simple components
|
||||
3. **Callback types** - Full signature with params and return type
|
||||
4. **Generics** - Use for reusable components/hooks
|
||||
5. **Const assertions** - `as const` for constant objects/arrays
|
||||
6. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
|
||||
2. **Props interface** - Always define for components
|
||||
3. **Const assertions** - `as const` for constant objects/arrays
|
||||
4. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
|
||||
5. **Type imports** - `import type { X }` for type-only imports
|
||||
|
||||
## Anti-Patterns
|
||||
```typescript
|
||||
// ❌ Bad
|
||||
// ✗ Bad
|
||||
const handleClick = (e: any) => {}
|
||||
useEffect(() => { doSomething(prop) }, []) // Missing dep
|
||||
|
||||
// ✅ Good
|
||||
// ✓ Good
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
|
||||
useEffect(() => { doSomething(prop) }, [prop])
|
||||
```
|
||||
|
||||
304
CLAUDE.md
@@ -1,47 +1,295 @@
|
||||
# Expert Programming Standards
|
||||
# Sim Studio Development Guidelines
|
||||
|
||||
**You are tasked with implementing solutions that follow best practices. You MUST be accurate, elegant, and efficient as an expert programmer.**
|
||||
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||
|
||||
---
|
||||
## Global Standards
|
||||
|
||||
# Role
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||
- **Styling**: Never update global styles. Keep all styling local to components
|
||||
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||
|
||||
You are a professional software engineer. All code you write MUST follow best practices, ensuring accuracy, quality, readability, and cleanliness. You MUST make FOCUSED EDITS that are EFFICIENT and ELEGANT.
|
||||
## Architecture
|
||||
|
||||
## Logs
|
||||
### Core Principles
|
||||
1. Single Responsibility: Each component, hook, store has one clear purpose
|
||||
2. Composition Over Complexity: Break down complex logic into smaller pieces
|
||||
3. Type Safety First: TypeScript interfaces for all props, state, return types
|
||||
4. Predictable State: Zustand for global state, useState for UI-only concerns
|
||||
|
||||
ENSURE that you use the logger.info and logger.warn and logger.error instead of the console.log whenever you want to display logs.
|
||||
### Root Structure
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
```
|
||||
|
||||
## Comments
|
||||
### Naming Conventions
|
||||
- Components: PascalCase (`WorkflowList`)
|
||||
- Hooks: `use` prefix (`useWorkflowOperations`)
|
||||
- Files: kebab-case (`workflow-list.tsx`)
|
||||
- Stores: `stores/feature/store.ts`
|
||||
- Constants: SCREAMING_SNAKE_CASE
|
||||
- Interfaces: PascalCase with suffix (`WorkflowListProps`)
|
||||
|
||||
You must use TSDOC for comments. Do not use ==== for comments to separate sections. Do not leave any comments that are not TSDOC.
|
||||
## Imports
|
||||
|
||||
## Global Styles
|
||||
**Always use absolute imports.** Never use relative imports.
|
||||
|
||||
You should not update the global styles unless it is absolutely necessary. Keep all styling local to components and files.
|
||||
```typescript
|
||||
// ✓ Good
|
||||
import { useWorkflowStore } from '@/stores/workflows/store'
|
||||
|
||||
## Bun
|
||||
// ✗ Bad
|
||||
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||
```
|
||||
|
||||
Use bun and bunx not npm and npx.
|
||||
Use barrel exports (`index.ts`) when a folder has 3+ exports.
|
||||
|
||||
## Code Quality
|
||||
### Import Order
|
||||
1. React/core libraries
|
||||
2. External libraries
|
||||
3. UI components (`@/components/emcn`, `@/components/ui`)
|
||||
4. Utilities (`@/lib/...`)
|
||||
5. Stores (`@/stores/...`)
|
||||
6. Feature imports
|
||||
7. CSS imports
|
||||
|
||||
- Write clean, maintainable code that follows the project's existing patterns
|
||||
- Prefer composition over inheritance
|
||||
- Keep functions small and focused on a single responsibility
|
||||
- Use meaningful variable and function names
|
||||
- Handle errors gracefully and provide useful error messages
|
||||
- Write type-safe code with proper TypeScript types
|
||||
Use `import type { X }` for type-only imports.
|
||||
|
||||
## TypeScript
|
||||
|
||||
1. No `any` - Use proper types or `unknown` with type guards
|
||||
2. Always define props interface for components
|
||||
3. `as const` for constant objects/arrays
|
||||
4. Explicit ref types: `useRef<HTMLDivElement>(null)`
|
||||
|
||||
## Components
|
||||
|
||||
```typescript
|
||||
'use client' // Only if using hooks
|
||||
|
||||
const CONFIG = { SPACING: 8 } as const
|
||||
|
||||
interface ComponentProps {
|
||||
requiredProp: string
|
||||
optionalProp?: boolean
|
||||
}
|
||||
|
||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||
// Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
|
||||
}
|
||||
```
|
||||
|
||||
Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.
|
||||
|
||||
## Hooks
|
||||
|
||||
```typescript
|
||||
interface UseFeatureProps { id: string }
|
||||
|
||||
export function useFeature({ id }: UseFeatureProps) {
|
||||
const idRef = useRef(id)
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
|
||||
useEffect(() => { idRef.current = id }, [id])
|
||||
|
||||
const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
|
||||
|
||||
return { data, fetchData }
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand Stores
|
||||
|
||||
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
const initialState = { items: [] as Item[] }
|
||||
|
||||
export const useFeatureStore = create<FeatureState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
setItems: (items) => set({ items }),
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
{ name: 'feature-store' }
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.
|
||||
|
||||
## React Query
|
||||
|
||||
All React Query hooks live in `hooks/queries/`.
|
||||
|
||||
```typescript
|
||||
export const entityKeys = {
|
||||
all: ['entity'] as const,
|
||||
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
|
||||
}
|
||||
|
||||
export function useEntityList(workspaceId?: string) {
|
||||
return useQuery({
|
||||
queryKey: entityKeys.list(workspaceId),
|
||||
queryFn: () => fetchEntities(workspaceId as string),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||
|
||||
```typescript
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
```
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
|
||||
## Testing
|
||||
|
||||
- Write tests for new functionality when appropriate
|
||||
- Ensure existing tests pass before completing work
|
||||
- Follow the project's testing conventions
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
|
||||
## Performance
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
- Consider performance implications of your code
|
||||
- Avoid unnecessary re-renders in React components
|
||||
- Use appropriate data structures and algorithms
|
||||
- Profile and optimize when necessary
|
||||
// Mocks BEFORE imports
|
||||
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
|
||||
|
||||
// Imports AFTER mocks
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createSession, loggerMock } from '@sim/testing'
|
||||
|
||||
describe('feature', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('runs in parallel', () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
Use `@sim/testing` factories over manual test data.
|
||||
|
||||
## Utils Rules
|
||||
|
||||
- Never create `utils.ts` for single consumer - inline it
|
||||
- Create `utils.ts` when 2+ files need the same helper
|
||||
- Check existing sources in `lib/` before duplicating
|
||||
|
||||
## Adding Integrations
|
||||
|
||||
New integrations require: **Tools** → **Block** → **Icon** → (optional) **Trigger**
|
||||
|
||||
Always look up the service's API docs first.
|
||||
|
||||
### 1. Tools (`tools/{service}/`)
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── types.ts # Params/response types
|
||||
└── {action}.ts # Tool implementation
|
||||
```
|
||||
|
||||
**Tool structure:**
|
||||
```typescript
|
||||
export const serviceTool: ToolConfig<Params, Response> = {
|
||||
id: 'service_action',
|
||||
name: 'Service Action',
|
||||
description: '...',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: 'service' },
|
||||
params: { /* ... */ },
|
||||
request: { url: '/api/tools/service/action', method: 'POST', ... },
|
||||
transformResponse: async (response) => { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `tools/registry.ts`.
|
||||
|
||||
### 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
type: 'service',
|
||||
name: 'Service',
|
||||
description: '...',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: ServiceIcon,
|
||||
subBlocks: [ /* see SubBlock Properties */ ],
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
|
||||
inputs: { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `blocks/registry.ts` (alphabetically).
|
||||
|
||||
**SubBlock Properties:**
|
||||
```typescript
|
||||
{
|
||||
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
|
||||
required: true, // or condition object
|
||||
condition: { field: 'op', value: 'send' }, // show/hide
|
||||
dependsOn: ['credential'], // clear when dep changes
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**condition examples:**
|
||||
- `{ field: 'op', value: 'send' }` - show when op === 'send'
|
||||
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
|
||||
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
|
||||
- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex
|
||||
|
||||
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
||||
|
||||
### 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function ServiceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return <svg {...props}>/* SVG from brand assets */</svg>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── webhook.ts # Webhook handler
|
||||
└── {event}.ts # Event-specific handlers
|
||||
```
|
||||
|
||||
Register in `triggers/registry.ts`.
|
||||
|
||||
### Integration Checklist
|
||||
|
||||
- [ ] Look up API docs
|
||||
- [ ] Create `tools/{service}/` with types and tools
|
||||
- [ ] Register tools in `tools/registry.ts`
|
||||
- [ ] Add icon to `components/icons.tsx`
|
||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||
- [ ] Register block in `blocks/registry.ts`
|
||||
- [ ] (Optional) Create and register triggers
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
IntercomIcon,
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
KalshiIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -168,6 +169,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
intercom: IntercomIcon,
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
kalshi: KalshiIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
linear: LinearIcon,
|
||||
|
||||
108
apps/docs/content/docs/de/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Workflows als MCP bereitstellen
|
||||
description: Stellen Sie Ihre Workflows als MCP-Tools für externe KI-Assistenten
|
||||
und Anwendungen bereit
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Stellen Sie Ihre Workflows als MCP-Tools bereit, um sie für externe KI-Assistenten wie Claude Desktop, Cursor und andere MCP-kompatible Clients zugänglich zu machen. Dies verwandelt Ihre Workflows in aufrufbare Tools, die von überall aus aufgerufen werden können.
|
||||
|
||||
## MCP-Server erstellen und verwalten
|
||||
|
||||
MCP-Server gruppieren Ihre Workflow-Tools zusammen. Erstellen und verwalten Sie sie in den Workspace-Einstellungen:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navigieren Sie zu **Einstellungen → MCP-Server**
|
||||
2. Klicken Sie auf **Server erstellen**
|
||||
3. Geben Sie einen Namen und eine optionale Beschreibung ein
|
||||
4. Kopieren Sie die Server-URL zur Verwendung in Ihren MCP-Clients
|
||||
5. Zeigen Sie alle zum Server hinzugefügten Tools an und verwalten Sie diese
|
||||
|
||||
## Einen Workflow als Tool hinzufügen
|
||||
|
||||
Sobald Ihr Workflow bereitgestellt ist, können Sie ihn als MCP-Tool verfügbar machen:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Öffnen Sie Ihren bereitgestellten Workflow
|
||||
2. Klicken Sie auf **Bereitstellen** und wechseln Sie zum Tab **MCP**
|
||||
3. Konfigurieren Sie den Tool-Namen und die Beschreibung
|
||||
4. Fügen Sie Beschreibungen für jeden Parameter hinzu (hilft der KI, Eingaben zu verstehen)
|
||||
5. Wählen Sie aus, zu welchen MCP-Servern es hinzugefügt werden soll
|
||||
|
||||
<Callout type="info">
|
||||
Der Workflow muss bereitgestellt sein, bevor er als MCP-Tool hinzugefügt werden kann.
|
||||
</Callout>
|
||||
|
||||
## Tool-Konfiguration
|
||||
|
||||
### Tool-Name
|
||||
Verwenden Sie Kleinbuchstaben, Zahlen und Unterstriche. Der Name sollte beschreibend sein und den MCP-Namenskonventionen folgen (z. B. `search_documents`, `send_email`).
|
||||
|
||||
### Beschreibung
|
||||
Schreiben Sie eine klare Beschreibung dessen, was das Tool tut. Dies hilft KI-Assistenten zu verstehen, wann das Tool verwendet werden soll.
|
||||
|
||||
### Parameter
|
||||
Die Eingabeformatfelder deines Workflows werden zu Tool-Parametern. Füge jedem Parameter Beschreibungen hinzu, um KI-Assistenten zu helfen, korrekte Werte bereitzustellen.
|
||||
|
||||
## MCP-Clients verbinden
|
||||
|
||||
Verwende die Server-URL aus den Einstellungen, um externe Anwendungen zu verbinden:
|
||||
|
||||
### Claude Desktop
|
||||
Füge dies zu deiner Claude Desktop-Konfiguration hinzu (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
Füge die Server-URL in den MCP-Einstellungen von Cursor mit demselben mcp-remote-Muster hinzu.
|
||||
|
||||
<Callout type="warn">
|
||||
Füge deinen API-Key-Header (`X-API-Key`) für authentifizierten Zugriff hinzu, wenn du mcp-remote oder andere HTTP-basierte MCP-Transporte verwendest.
|
||||
</Callout>
|
||||
|
||||
## Server-Verwaltung
|
||||
|
||||
In der Server-Detailansicht unter **Einstellungen → MCP-Server** kannst du:
|
||||
|
||||
- **Tools anzeigen**: Alle Workflows sehen, die einem Server hinzugefügt wurden
|
||||
- **URL kopieren**: Die Server-URL für MCP-Clients abrufen
|
||||
- **Workflows hinzufügen**: Weitere bereitgestellte Workflows als Tools hinzufügen
|
||||
- **Tools entfernen**: Workflows vom Server entfernen
|
||||
- **Server löschen**: Den gesamten Server und alle seine Tools entfernen
|
||||
|
||||
## So funktioniert es
|
||||
|
||||
Wenn ein MCP-Client dein Tool aufruft:
|
||||
|
||||
1. Die Anfrage wird an deiner MCP-Server-URL empfangen
|
||||
2. Sim validiert die Anfrage und ordnet Parameter den Workflow-Eingaben zu
|
||||
3. Der bereitgestellte Workflow wird mit den angegebenen Eingaben ausgeführt
|
||||
4. Die Ergebnisse werden an den MCP-Client zurückgegeben
|
||||
|
||||
Workflows werden mit derselben Bereitstellungsversion wie API-Aufrufe ausgeführt, was konsistentes Verhalten gewährleistet.
|
||||
|
||||
## Berechtigungsanforderungen
|
||||
|
||||
| Aktion | Erforderliche Berechtigung |
|
||||
|--------|-------------------|
|
||||
| MCP-Server erstellen | **Admin** |
|
||||
| Workflows zu Servern hinzufügen | **Write** oder **Admin** |
|
||||
| MCP-Server anzeigen | **Read**, **Write** oder **Admin** |
|
||||
| MCP-Server löschen | **Admin** |
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: MCP (Model Context Protocol)
|
||||
title: MCP-Tools verwenden
|
||||
description: Externe Tools und Dienste über das Model Context Protocol verbinden
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Das Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) ermöglicht es Ihnen, externe Tools und Dienste über ein standardisiertes Protokoll zu verbinden, wodurch Sie APIs und Dienste direkt in Ihre Workflows integrieren können. Mit MCP können Sie die Fähigkeiten von Sim erweitern, indem Sie benutzerdefinierte Integrationen hinzufügen, die nahtlos mit Ihren Agenten und Workflows zusammenarbeiten.
|
||||
@@ -20,14 +22,8 @@ MCP ist ein offener Standard, der es KI-Assistenten ermöglicht, sich sicher mit
|
||||
|
||||
MCP-Server stellen Sammlungen von Tools bereit, die Ihre Agenten nutzen können. Konfigurieren Sie diese in den Workspace-Einstellungen:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="Konfiguration eines MCP-Servers in den Einstellungen"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navigieren Sie zu Ihren Workspace-Einstellungen
|
||||
@@ -40,14 +36,18 @@ MCP-Server stellen Sammlungen von Tools bereit, die Ihre Agenten nutzen können.
|
||||
Sie können MCP-Server auch direkt über die Symbolleiste in einem Agent-Block für eine schnelle Einrichtung konfigurieren.
|
||||
</Callout>
|
||||
|
||||
## Verwendung von MCP-Tools in Agenten
|
||||
### Tools aktualisieren
|
||||
|
||||
Sobald MCP-Server konfiguriert sind, werden ihre Tools innerhalb Ihrer Agent-Blöcke verfügbar:
|
||||
Klicken Sie bei einem Server auf **Aktualisieren**, um die neuesten Tool-Schemas abzurufen und alle Agent-Blöcke, die diese Tools verwenden, automatisch mit den neuen Parameterdefinitionen zu aktualisieren.
|
||||
|
||||
## MCP-Tools in Agents verwenden
|
||||
|
||||
Sobald MCP-Server konfiguriert sind, werden ihre Tools in Ihren Agent-Blöcken verfügbar:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-2.png"
|
||||
alt="Verwendung eines MCP-Tools im Agent-Block"
|
||||
alt="Using MCP Tool in Agent Block"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
@@ -55,25 +55,25 @@ Sobald MCP-Server konfiguriert sind, werden ihre Tools innerhalb Ihrer Agent-Bl
|
||||
</div>
|
||||
|
||||
1. Öffnen Sie einen **Agent**-Block
|
||||
2. Im Abschnitt **Tools** sehen Sie die verfügbaren MCP-Tools
|
||||
2. Im Bereich **Tools** sehen Sie die verfügbaren MCP-Tools
|
||||
3. Wählen Sie die Tools aus, die der Agent verwenden soll
|
||||
4. Der Agent kann nun während der Ausführung auf diese Tools zugreifen
|
||||
|
||||
## Eigenständiger MCP-Tool-Block
|
||||
|
||||
Für eine genauere Kontrolle können Sie den dedizierten MCP-Tool-Block verwenden, um bestimmte MCP-Tools auszuführen:
|
||||
Für eine präzisere Steuerung können Sie den dedizierten MCP-Tool-Block verwenden, um bestimmte MCP-Tools auszuführen:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-3.png"
|
||||
alt="Eigenständiger MCP-Tool-Block"
|
||||
alt="Standalone MCP Tool Block"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Der MCP-Tool-Block ermöglicht es Ihnen:
|
||||
Der MCP-Tool-Block ermöglicht Ihnen:
|
||||
- Jedes konfigurierte MCP-Tool direkt auszuführen
|
||||
- Spezifische Parameter an das Tool zu übergeben
|
||||
- Die Ausgabe des Tools in nachfolgenden Workflow-Schritten zu verwenden
|
||||
@@ -81,9 +81,9 @@ Der MCP-Tool-Block ermöglicht es Ihnen:
|
||||
|
||||
### Wann MCP-Tool vs. Agent verwenden
|
||||
|
||||
**Verwenden Sie einen Agenten mit MCP-Tools, wenn:**
|
||||
- Sie möchten, dass die KI entscheidet, welche Tools zu verwenden sind
|
||||
- Sie komplexe Überlegungen benötigen, wann und wie Tools eingesetzt werden sollen
|
||||
**Verwenden Sie Agent mit MCP-Tools, wenn:**
|
||||
- Sie möchten, dass die KI entscheidet, welche Tools verwendet werden
|
||||
- Sie komplexes Reasoning darüber benötigen, wann und wie Tools verwendet werden
|
||||
- Sie eine natürlichsprachliche Interaktion mit den Tools wünschen
|
||||
|
||||
**Verwenden Sie den MCP-Tool-Block, wenn:**
|
||||
@@ -93,7 +93,7 @@ Der MCP-Tool-Block ermöglicht es Ihnen:
|
||||
|
||||
## Berechtigungsanforderungen
|
||||
|
||||
MCP-Funktionalität erfordert spezifische Workspace-Berechtigungen:
|
||||
Die MCP-Funktionalität erfordert spezifische Workspace-Berechtigungen:
|
||||
|
||||
| Aktion | Erforderliche Berechtigung |
|
||||
|--------|-------------------|
|
||||
@@ -105,7 +105,7 @@ MCP-Funktionalität erfordert spezifische Workspace-Berechtigungen:
|
||||
## Häufige Anwendungsfälle
|
||||
|
||||
### Datenbankintegration
|
||||
Verbinden Sie sich mit Datenbanken, um Daten innerhalb Ihrer Workflows abzufragen, einzufügen oder zu aktualisieren.
|
||||
Verbinden Sie sich mit Datenbanken, um Daten in Ihren Workflows abzufragen, einzufügen oder zu aktualisieren.
|
||||
|
||||
### API-Integrationen
|
||||
Greifen Sie auf externe APIs und Webdienste zu, die keine integrierten Sim-Integrationen haben.
|
||||
@@ -113,8 +113,8 @@ Greifen Sie auf externe APIs und Webdienste zu, die keine integrierten Sim-Integ
|
||||
### Dateisystemzugriff
|
||||
Lesen, schreiben und bearbeiten Sie Dateien auf lokalen oder entfernten Dateisystemen.
|
||||
|
||||
### Benutzerdefinierte Geschäftslogik
|
||||
Führen Sie benutzerdefinierte Skripte oder Tools aus, die auf die Bedürfnisse Ihrer Organisation zugeschnitten sind.
|
||||
### Individuelle Geschäftslogik
|
||||
Führen Sie benutzerdefinierte Skripte oder Tools aus, die spezifisch für die Anforderungen Ihrer Organisation sind.
|
||||
|
||||
### Echtzeit-Datenzugriff
|
||||
Rufen Sie Live-Daten von externen Systemen während der Workflow-Ausführung ab.
|
||||
@@ -128,12 +128,12 @@ Rufen Sie Live-Daten von externen Systemen während der Workflow-Ausführung ab.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### MCP-Server erscheint nicht
|
||||
### MCP-Server wird nicht angezeigt
|
||||
- Überprüfen Sie, ob die Serverkonfiguration korrekt ist
|
||||
- Prüfen Sie, ob Sie die erforderlichen Berechtigungen haben
|
||||
- Stellen Sie sicher, dass der MCP-Server läuft und zugänglich ist
|
||||
- Prüfen Sie, ob Sie über die erforderlichen Berechtigungen verfügen
|
||||
- Stellen Sie sicher, dass der MCP-Server läuft und erreichbar ist
|
||||
|
||||
### Fehler bei der Tool-Ausführung
|
||||
### Tool-Ausführungsfehler
|
||||
- Überprüfen Sie, ob die Tool-Parameter korrekt formatiert sind
|
||||
- Prüfen Sie die MCP-Server-Logs auf Fehlermeldungen
|
||||
- Stellen Sie sicher, dass die erforderliche Authentifizierung konfiguriert ist
|
||||
@@ -141,4 +141,4 @@ Rufen Sie Live-Daten von externen Systemen während der Workflow-Ausführung ab.
|
||||
### Berechtigungsfehler
|
||||
- Bestätigen Sie Ihre Workspace-Berechtigungsstufe
|
||||
- Prüfen Sie, ob der MCP-Server zusätzliche Authentifizierung erfordert
|
||||
- Stellen Sie sicher, dass der Server für Ihren Workspace richtig konfiguriert ist
|
||||
- Überprüfen Sie, ob der Server ordnungsgemäß für Ihren Workspace konfiguriert ist
|
||||
486
apps/docs/content/docs/de/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: Interagieren Sie mit Jira Service Management
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Nutzungsanweisungen
|
||||
|
||||
Integrieren Sie Jira Service Management für IT-Service-Management. Erstellen und verwalten Sie Service-Anfragen, bearbeiten Sie Kunden und Organisationen, verfolgen Sie SLAs und verwalten Sie Warteschlangen.
|
||||
|
||||
## Tools
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
Alle Service Desks aus Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `serviceDesks` | json | Array von Service Desks |
|
||||
| `total` | number | Gesamtanzahl der Service Desks |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
Anfragetypen für einen Service Desk in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service Desk ID, für die Anfragetypen abgerufen werden sollen |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `requestTypes` | json | Array von Anfragetypen |
|
||||
| `total` | number | Gesamtanzahl der Anfragetypen |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
Erstellen Sie eine neue Serviceanfrage in Jira Service Management
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service-Desk-ID, in der die Anfrage erstellt werden soll |
|
||||
| `requestTypeId` | string | Ja | Anfragetyp-ID für die neue Anfrage |
|
||||
| `summary` | string | Ja | Zusammenfassung/Titel für die Serviceanfrage |
|
||||
| `description` | string | Nein | Beschreibung für die Serviceanfrage |
|
||||
| `raiseOnBehalfOf` | string | Nein | Konto-ID des Kunden, für den die Anfrage im Namen gestellt werden soll |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueId` | string | Erstellte Anfrage-Issue-ID |
|
||||
| `issueKey` | string | Erstellter Anfrage-Issue-Key \(z. B. SD-123\) |
|
||||
| `requestTypeId` | string | Anfragetyp-ID |
|
||||
| `serviceDeskId` | string | Service-Desk-ID |
|
||||
| `success` | boolean | Ob die Anfrage erfolgreich erstellt wurde |
|
||||
| `url` | string | URL zur erstellten Anfrage |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
Eine einzelne Serviceanfrage aus Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
Mehrere Serviceanfragen aus Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `serviceDeskId` | string | Nein | Nach Service-Desk-ID filtern |
|
||||
| `requestOwnership` | string | Nein | Nach Eigentümerschaft filtern: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
|
||||
| `requestStatus` | string | Nein | Nach Status filtern: OPEN, CLOSED, ALL |
|
||||
| `searchTerm` | string | Nein | Suchbegriff zum Filtern von Anfragen |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `requests` | json | Array von Serviceanfragen |
|
||||
| `total` | number | Gesamtanzahl der Anfragen |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
Einen Kommentar (öffentlich oder intern) zu einer Serviceanfrage in Jira Service Management hinzufügen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `body` | string | Ja | Kommentartext |
|
||||
| `isPublic` | boolean | Ja | Ob der Kommentar öffentlich \(für Kunden sichtbar\) oder intern ist |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `commentId` | string | ID des erstellten Kommentars |
|
||||
| `body` | string | Kommentartext |
|
||||
| `isPublic` | boolean | Ob der Kommentar öffentlich ist |
|
||||
| `success` | boolean | Ob der Kommentar erfolgreich hinzugefügt wurde |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
Kommentare für eine Serviceanfrage in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `isPublic` | boolean | Nein | Nur öffentliche Kommentare filtern |
|
||||
| `internal` | boolean | Nein | Nur interne Kommentare filtern |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `comments` | json | Array von Kommentaren |
|
||||
| `total` | number | Gesamtanzahl der Kommentare |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
Kunden für einen Service Desk in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service-Desk-ID, für die Kunden abgerufen werden sollen |
|
||||
| `query` | string | Nein | Suchabfrage zum Filtern von Kunden |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `customers` | json | Array von Kunden |
|
||||
| `total` | number | Gesamtanzahl der Kunden |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
Kunden zu einem Service Desk in Jira Service Management hinzufügen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service-Desk-ID, zu der Kunden hinzugefügt werden sollen |
|
||||
| `emails` | string | Ja | Kommagetrennte E-Mail-Adressen, die als Kunden hinzugefügt werden sollen |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `serviceDeskId` | string | Service-Desk-ID |
|
||||
| `success` | boolean | Ob Kunden erfolgreich hinzugefügt wurden |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
Organisationen für einen Service Desk in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service-Desk-ID, für die Organisationen abgerufen werden sollen |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `organizations` | json | Array von Organisationen |
|
||||
| `total` | number | Gesamtanzahl der Organisationen |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
Eine neue Organisation in Jira Service Management erstellen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `name` | string | Ja | Name der zu erstellenden Organisation |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `organizationId` | string | ID der erstellten Organisation |
|
||||
| `name` | string | Name der erstellten Organisation |
|
||||
| `success` | boolean | Ob die Operation erfolgreich war |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
Eine Organisation zu einem Service Desk in Jira Service Management hinzufügen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service Desk ID, zu der die Organisation hinzugefügt werden soll |
|
||||
| `organizationId` | string | Ja | Organisations-ID, die zum Service Desk hinzugefügt werden soll |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `serviceDeskId` | string | Service Desk ID |
|
||||
| `organizationId` | string | Hinzugefügte Organisations-ID |
|
||||
| `success` | boolean | Ob die Operation erfolgreich war |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
Warteschlangen für einen Service Desk in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `serviceDeskId` | string | Ja | Service Desk ID, für die Warteschlangen abgerufen werden sollen |
|
||||
| `includeCount` | boolean | Nein | Vorgangsanzahl für jede Warteschlange einbeziehen |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `queues` | json | Array von Warteschlangen |
|
||||
| `total` | number | Gesamtanzahl der Warteschlangen |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
SLA-Informationen für eine Serviceanfrage in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `slas` | json | Array mit SLA-Informationen |
|
||||
| `total` | number | Gesamtanzahl der SLAs |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
Verfügbare Übergänge für eine Serviceanfrage in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `transitions` | json | Array mit verfügbaren Übergängen |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
Eine Serviceanfrage in einen neuen Status in Jira Service Management überführen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `transitionId` | string | Ja | Anzuwendende Übergangs-ID |
|
||||
| `comment` | string | Nein | Optionaler Kommentar, der während des Übergangs hinzugefügt werden kann |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Issue-ID oder -Schlüssel |
|
||||
| `transitionId` | string | Angewendete Übergangs-ID |
|
||||
| `success` | boolean | Ob der Übergang erfolgreich war |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
Teilnehmer für eine Anfrage in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Issue-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Issue-ID oder -Schlüssel |
|
||||
| `participants` | json | Array von Teilnehmern |
|
||||
| `total` | number | Gesamtanzahl der Teilnehmer |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
Teilnehmer zu einer Anfrage in Jira Service Management hinzufügen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Issue-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `accountIds` | string | Ja | Durch Kommas getrennte Account-IDs, die als Teilnehmer hinzugefügt werden sollen |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `participants` | json | Array der hinzugefügten Teilnehmer |
|
||||
| `success` | boolean | Ob die Operation erfolgreich war |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
Genehmigungen für eine Anfrage in Jira Service Management abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `start` | number | Nein | Startindex für Paginierung \(Standard: 0\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl zurückzugebender Ergebnisse \(Standard: 50\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Vorgangs-ID oder -Schlüssel |
|
||||
| `approvals` | json | Array der Genehmigungen |
|
||||
| `total` | number | Gesamtanzahl der Genehmigungen |
|
||||
| `isLastPage` | boolean | Ob dies die letzte Seite ist |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
Eine Genehmigungsanfrage in Jira Service Management genehmigen oder ablehnen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Ja | Ihre Jira-Domain \(z. B. ihrfirma.atlassian.net\) |
|
||||
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz |
|
||||
| `issueIdOrKey` | string | Ja | Vorgangs-ID oder -Schlüssel \(z. B. SD-123\) |
|
||||
| `approvalId` | string | Ja | Genehmigungs-ID zur Beantwortung |
|
||||
| `decision` | string | Ja | Entscheidung: "approve" oder "decline" |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueIdOrKey` | string | Issue-ID oder -Schlüssel |
|
||||
| `approvalId` | string | Genehmigungs-ID |
|
||||
| `decision` | string | Getroffene Entscheidung \(genehmigen/ablehnen\) |
|
||||
| `success` | boolean | Ob die Operation erfolgreich war |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
- Typ: `jira_service_management`
|
||||
108
apps/docs/content/docs/en/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Deploy Workflows as MCP
|
||||
description: Expose your workflows as MCP tools for external AI assistants and applications
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Deploy your workflows as MCP tools to make them accessible to external AI assistants like Claude Desktop, Cursor, and other MCP-compatible clients. This turns your workflows into callable tools that can be invoked from anywhere.
|
||||
|
||||
## Creating and Managing MCP Servers
|
||||
|
||||
MCP servers group your workflow tools together. Create and manage them in workspace settings:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navigate to **Settings → MCP Servers**
|
||||
2. Click **Create Server**
|
||||
3. Enter a name and optional description
|
||||
4. Copy the server URL for use in your MCP clients
|
||||
5. View and manage all tools added to the server
|
||||
|
||||
## Adding a Workflow as a Tool
|
||||
|
||||
Once your workflow is deployed, you can expose it as an MCP tool:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Open your deployed workflow
|
||||
2. Click **Deploy** and go to the **MCP** tab
|
||||
3. Configure the tool name and description
|
||||
4. Add descriptions for each parameter (helps AI understand inputs)
|
||||
5. Select which MCP servers to add it to
|
||||
|
||||
<Callout type="info">
|
||||
The workflow must be deployed before it can be added as an MCP tool.
|
||||
</Callout>
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### Tool Name
|
||||
Use lowercase letters, numbers, and underscores. The name should be descriptive and follow MCP naming conventions (e.g., `search_documents`, `send_email`).
|
||||
|
||||
### Description
|
||||
Write a clear description of what the tool does. This helps AI assistants understand when to use the tool.
|
||||
|
||||
### Parameters
|
||||
Your workflow's input format fields become tool parameters. Add descriptions to each parameter to help AI assistants provide correct values.
|
||||
|
||||
## Connecting MCP Clients
|
||||
|
||||
Use the server URL from settings to connect external applications:
|
||||
|
||||
### Claude Desktop
|
||||
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
Add the server URL in Cursor's MCP settings using the same mcp-remote pattern.
|
||||
|
||||
<Callout type="warn">
|
||||
Include your API key header (`X-API-Key`) for authenticated access when using mcp-remote or other HTTP-based MCP transports.
|
||||
</Callout>
|
||||
|
||||
## Server Management
|
||||
|
||||
From the server detail view in **Settings → MCP Servers**, you can:
|
||||
|
||||
- **View tools**: See all workflows added to a server
|
||||
- **Copy URL**: Get the server URL for MCP clients
|
||||
- **Add workflows**: Add more deployed workflows as tools
|
||||
- **Remove tools**: Remove workflows from the server
|
||||
- **Delete server**: Remove the entire server and all its tools
|
||||
|
||||
## How It Works
|
||||
|
||||
When an MCP client calls your tool:
|
||||
|
||||
1. The request is received at your MCP server URL
|
||||
2. Sim validates the request and maps parameters to workflow inputs
|
||||
3. The deployed workflow executes with the provided inputs
|
||||
4. Results are returned to the MCP client
|
||||
|
||||
Workflows execute using the same deployment version as API calls, ensuring consistent behavior.
|
||||
|
||||
## Permission Requirements
|
||||
|
||||
| Action | Required Permission |
|
||||
|--------|-------------------|
|
||||
| Create MCP servers | **Admin** |
|
||||
| Add workflows to servers | **Write** or **Admin** |
|
||||
| View MCP servers | **Read**, **Write**, or **Admin** |
|
||||
| Delete MCP servers | **Admin** |
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: MCP (Model Context Protocol)
|
||||
title: Using MCP Tools
|
||||
description: Connect external tools and services using the Model Context Protocol
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
The Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) allows you to connect external tools and services using a standardized protocol, enabling you to integrate APIs and services directly into your workflows. With MCP, you can extend Sim's capabilities by adding custom integrations that work seamlessly with your agents and workflows.
|
||||
@@ -20,14 +22,8 @@ MCP is an open standard that enables AI assistants to securely connect to extern
|
||||
|
||||
MCP servers provide collections of tools that your agents can use. Configure them in workspace settings:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="Configuring MCP Server in Settings"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navigate to your workspace settings
|
||||
@@ -40,6 +36,10 @@ MCP servers provide collections of tools that your agents can use. Configure the
|
||||
You can also configure MCP servers directly from the toolbar in an Agent block for quick setup.
|
||||
</Callout>
|
||||
|
||||
### Refresh Tools
|
||||
|
||||
Click **Refresh** on a server to fetch the latest tool schemas and automatically update any agent blocks using those tools with the new parameter definitions.
|
||||
|
||||
## Using MCP Tools in Agents
|
||||
|
||||
Once MCP servers are configured, their tools become available within your agent blocks:
|
||||
|
||||
5
apps/docs/content/docs/en/mcp/meta.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "MCP",
|
||||
"pages": ["index", "deploy-workflows"],
|
||||
"defaultOpen": false
|
||||
}
|
||||
490
apps/docs/content/docs/en/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: Interact with Jira Service Management
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate with Jira Service Management for IT service management. Create and manage service requests, handle customers and organizations, track SLAs, and manage queues.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
Get all service desks from Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `serviceDesks` | json | Array of service desks |
|
||||
| `total` | number | Total number of service desks |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
Get request types for a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to get request types for |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `requestTypes` | json | Array of request types |
|
||||
| `total` | number | Total number of request types |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
Create a new service request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to create the request in |
|
||||
| `requestTypeId` | string | Yes | Request Type ID for the new request |
|
||||
| `summary` | string | Yes | Summary/title for the service request |
|
||||
| `description` | string | No | Description for the service request |
|
||||
| `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueId` | string | Created request issue ID |
|
||||
| `issueKey` | string | Created request issue key \(e.g., SD-123\) |
|
||||
| `requestTypeId` | string | Request type ID |
|
||||
| `serviceDeskId` | string | Service desk ID |
|
||||
| `success` | boolean | Whether the request was created successfully |
|
||||
| `url` | string | URL to the created request |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
Get a single service request from Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
Get multiple service requests from Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | No | Filter by service desk ID |
|
||||
| `requestOwnership` | string | No | Filter by ownership: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
|
||||
| `requestStatus` | string | No | Filter by status: OPEN, CLOSED, ALL |
|
||||
| `searchTerm` | string | No | Search term to filter requests |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `requests` | json | Array of service requests |
|
||||
| `total` | number | Total number of requests |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
Add a comment (public or internal) to a service request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `body` | string | Yes | Comment body text |
|
||||
| `isPublic` | boolean | Yes | Whether the comment is public \(visible to customer\) or internal |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `commentId` | string | Created comment ID |
|
||||
| `body` | string | Comment body text |
|
||||
| `isPublic` | boolean | Whether the comment is public |
|
||||
| `success` | boolean | Whether the comment was added successfully |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
Get comments for a service request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `isPublic` | boolean | No | Filter to only public comments |
|
||||
| `internal` | boolean | No | Filter to only internal comments |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `comments` | json | Array of comments |
|
||||
| `total` | number | Total number of comments |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
Get customers for a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to get customers for |
|
||||
| `query` | string | No | Search query to filter customers |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `customers` | json | Array of customers |
|
||||
| `total` | number | Total number of customers |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
Add customers to a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to add customers to |
|
||||
| `emails` | string | Yes | Comma-separated email addresses to add as customers |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `serviceDeskId` | string | Service desk ID |
|
||||
| `success` | boolean | Whether customers were added successfully |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
Get organizations for a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to get organizations for |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `organizations` | json | Array of organizations |
|
||||
| `total` | number | Total number of organizations |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
Create a new organization in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `name` | string | Yes | Name of the organization to create |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `organizationId` | string | ID of the created organization |
|
||||
| `name` | string | Name of the created organization |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
Add an organization to a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to add the organization to |
|
||||
| `organizationId` | string | Yes | Organization ID to add to the service desk |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `serviceDeskId` | string | Service Desk ID |
|
||||
| `organizationId` | string | Organization ID added |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
Get queues for a service desk in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID to get queues for |
|
||||
| `includeCount` | boolean | No | Include issue count for each queue |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `queues` | json | Array of queues |
|
||||
| `total` | number | Total number of queues |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
Get SLA information for a service request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `slas` | json | Array of SLA information |
|
||||
| `total` | number | Total number of SLAs |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
Get available transitions for a service request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `transitions` | json | Array of available transitions |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
Transition a service request to a new status in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `transitionId` | string | Yes | Transition ID to apply |
|
||||
| `comment` | string | No | Optional comment to add during transition |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `transitionId` | string | Applied transition ID |
|
||||
| `success` | boolean | Whether the transition was successful |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
Get participants for a request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `participants` | json | Array of participants |
|
||||
| `total` | number | Total number of participants |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
Add participants to a request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `accountIds` | string | Yes | Comma-separated account IDs to add as participants |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `participants` | json | Array of added participants |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
Get approvals for a request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `start` | number | No | Start index for pagination \(default: 0\) |
|
||||
| `limit` | number | No | Maximum results to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `approvals` | json | Array of approvals |
|
||||
| `total` | number | Total number of approvals |
|
||||
| `isLastPage` | boolean | Whether this is the last page |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
Approve or decline an approval request in Jira Service Management
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
|
||||
| `approvalId` | string | Yes | Approval ID to answer |
|
||||
| `decision` | string | Yes | Decision: "approve" or "decline" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueIdOrKey` | string | Issue ID or key |
|
||||
| `approvalId` | string | Approval ID |
|
||||
| `decision` | string | Decision made \(approve/decline\) |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `jira_service_management`
|
||||
@@ -46,6 +46,7 @@
|
||||
"intercom",
|
||||
"jina",
|
||||
"jira",
|
||||
"jira_service_management",
|
||||
"kalshi",
|
||||
"knowledge",
|
||||
"linear",
|
||||
|
||||
108
apps/docs/content/docs/es/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Implementar flujos de trabajo como MCP
|
||||
description: Expone tus flujos de trabajo como herramientas MCP para asistentes
|
||||
de IA externos y aplicaciones
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Implementa tus flujos de trabajo como herramientas MCP para hacerlos accesibles a asistentes de IA externos como Claude Desktop, Cursor y otros clientes compatibles con MCP. Esto convierte tus flujos de trabajo en herramientas invocables que pueden ser llamadas desde cualquier lugar.
|
||||
|
||||
## Crear y gestionar servidores MCP
|
||||
|
||||
Los servidores MCP agrupan tus herramientas de flujo de trabajo. Créalos y gestiόnalos en la configuración del espacio de trabajo:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navega a **Configuración → Servidores MCP**
|
||||
2. Haz clic en **Crear servidor**
|
||||
3. Introduce un nombre y una descripción opcional
|
||||
4. Copia la URL del servidor para usarla en tus clientes MCP
|
||||
5. Visualiza y gestiona todas las herramientas añadidas al servidor
|
||||
|
||||
## Añadir un flujo de trabajo como herramienta
|
||||
|
||||
Una vez que tu flujo de trabajo esté implementado, puedes exponerlo como una herramienta MCP:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Abre tu flujo de trabajo implementado
|
||||
2. Haz clic en **Implementar** y ve a la pestaña **MCP**
|
||||
3. Configura el nombre y la descripción de la herramienta
|
||||
4. Añade descripciones para cada parámetro (ayuda a la IA a entender las entradas)
|
||||
5. Selecciona a qué servidores MCP añadirla
|
||||
|
||||
<Callout type="info">
|
||||
El flujo de trabajo debe estar implementado antes de poder añadirse como herramienta MCP.
|
||||
</Callout>
|
||||
|
||||
## Configuración de la herramienta
|
||||
|
||||
### Nombre de la herramienta
|
||||
Usa letras minúsculas, números y guiones bajos. El nombre debe ser descriptivo y seguir las convenciones de nomenclatura de MCP (por ejemplo, `search_documents`, `send_email`).
|
||||
|
||||
### Descripción
|
||||
Escribe una descripción clara de lo que hace la herramienta. Esto ayuda a los asistentes de IA a entender cuándo usar la herramienta.
|
||||
|
||||
### Parámetros
|
||||
Los campos de formato de entrada de tu flujo de trabajo se convierten en parámetros de herramienta. Añade descripciones a cada parámetro para ayudar a los asistentes de IA a proporcionar valores correctos.
|
||||
|
||||
## Conectar clientes MCP
|
||||
|
||||
Usa la URL del servidor desde la configuración para conectar aplicaciones externas:
|
||||
|
||||
### Claude Desktop
|
||||
Añade a tu configuración de Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
Añade la URL del servidor en la configuración MCP de Cursor usando el mismo patrón mcp-remote.
|
||||
|
||||
<Callout type="warn">
|
||||
Incluye tu encabezado de clave API (`X-API-Key`) para acceso autenticado al usar mcp-remote u otros transportes MCP basados en HTTP.
|
||||
</Callout>
|
||||
|
||||
## Gestión del servidor
|
||||
|
||||
Desde la vista de detalle del servidor en **Configuración → Servidores MCP**, puedes:
|
||||
|
||||
- **Ver herramientas**: consulta todos los flujos de trabajo añadidos a un servidor
|
||||
- **Copiar URL**: obtén la URL del servidor para clientes MCP
|
||||
- **Añadir flujos de trabajo**: añade más flujos de trabajo desplegados como herramientas
|
||||
- **Eliminar herramientas**: elimina flujos de trabajo del servidor
|
||||
- **Eliminar servidor**: elimina el servidor completo y todas sus herramientas
|
||||
|
||||
## Cómo funciona
|
||||
|
||||
Cuando un cliente MCP llama a tu herramienta:
|
||||
|
||||
1. La solicitud se recibe en la URL de tu servidor MCP
|
||||
2. Sim valida la solicitud y mapea los parámetros a las entradas del flujo de trabajo
|
||||
3. El flujo de trabajo desplegado se ejecuta con las entradas proporcionadas
|
||||
4. Los resultados se devuelven al cliente MCP
|
||||
|
||||
Los flujos de trabajo se ejecutan usando la misma versión de despliegue que las llamadas API, garantizando un comportamiento consistente.
|
||||
|
||||
## Requisitos de permisos
|
||||
|
||||
| Acción | Permiso requerido |
|
||||
|--------|-------------------|
|
||||
| Crear servidores MCP | **Admin** |
|
||||
| Añadir flujos de trabajo a servidores | **Write** o **Admin** |
|
||||
| Ver servidores MCP | **Read**, **Write** o **Admin** |
|
||||
| Eliminar servidores MCP | **Admin** |
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: MCP (Protocolo de Contexto de Modelo)
|
||||
title: Uso de herramientas MCP
|
||||
description: Conecta herramientas y servicios externos usando el Model Context Protocol
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
El Protocolo de Contexto de Modelo ([MCP](https://modelcontextprotocol.com/)) te permite conectar herramientas y servicios externos utilizando un protocolo estandarizado, permitiéndote integrar APIs y servicios directamente en tus flujos de trabajo. Con MCP, puedes ampliar las capacidades de Sim añadiendo integraciones personalizadas que funcionan perfectamente con tus agentes y flujos de trabajo.
|
||||
@@ -20,14 +22,8 @@ MCP es un estándar abierto que permite a los asistentes de IA conectarse de for
|
||||
|
||||
Los servidores MCP proporcionan colecciones de herramientas que tus agentes pueden utilizar. Configúralos en los ajustes del espacio de trabajo:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="Configuración del servidor MCP en Ajustes"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Navega a los ajustes de tu espacio de trabajo
|
||||
@@ -40,14 +36,18 @@ Los servidores MCP proporcionan colecciones de herramientas que tus agentes pued
|
||||
También puedes configurar servidores MCP directamente desde la barra de herramientas en un bloque de Agente para una configuración rápida.
|
||||
</Callout>
|
||||
|
||||
### Actualizar herramientas
|
||||
|
||||
Haz clic en **Actualizar** en un servidor para obtener los esquemas de herramientas más recientes y actualizar automáticamente cualquier bloque de agente que use esas herramientas con las nuevas definiciones de parámetros.
|
||||
|
||||
## Uso de herramientas MCP en agentes
|
||||
|
||||
Una vez que los servidores MCP están configurados, sus herramientas estarán disponibles dentro de tus bloques de agente:
|
||||
Una vez configurados los servidores MCP, sus herramientas están disponibles dentro de tus bloques de agente:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-2.png"
|
||||
alt="Uso de herramienta MCP en bloque de Agente"
|
||||
alt="Uso de herramienta MCP en bloque de agente"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
@@ -61,7 +61,7 @@ Una vez que los servidores MCP están configurados, sus herramientas estarán di
|
||||
|
||||
## Bloque de herramienta MCP independiente
|
||||
|
||||
Para un control más preciso, puedes usar el bloque dedicado de Herramienta MCP para ejecutar herramientas MCP específicas:
|
||||
Para un control más granular, puedes usar el bloque de herramienta MCP dedicado para ejecutar herramientas MCP específicas:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -73,23 +73,23 @@ Para un control más preciso, puedes usar el bloque dedicado de Herramienta MCP
|
||||
/>
|
||||
</div>
|
||||
|
||||
El bloque de Herramienta MCP te permite:
|
||||
El bloque de herramienta MCP te permite:
|
||||
- Ejecutar cualquier herramienta MCP configurada directamente
|
||||
- Pasar parámetros específicos a la herramienta
|
||||
- Usar la salida de la herramienta en pasos posteriores del flujo de trabajo
|
||||
- Encadenar múltiples herramientas MCP
|
||||
|
||||
### Cuándo usar Herramienta MCP vs Agente
|
||||
### Cuándo usar herramienta MCP vs. agente
|
||||
|
||||
**Usa Agente con herramientas MCP cuando:**
|
||||
**Usa agente con herramientas MCP cuando:**
|
||||
- Quieres que la IA decida qué herramientas usar
|
||||
- Necesitas un razonamiento complejo sobre cuándo y cómo usar las herramientas
|
||||
- Deseas una interacción en lenguaje natural con las herramientas
|
||||
- Necesitas razonamiento complejo sobre cuándo y cómo usar las herramientas
|
||||
- Quieres interacción en lenguaje natural con las herramientas
|
||||
|
||||
**Usa el bloque Herramienta MCP cuando:**
|
||||
- Necesites una ejecución determinista de herramientas
|
||||
- Quieras ejecutar una herramienta específica con parámetros conocidos
|
||||
- Estés construyendo flujos de trabajo estructurados con pasos predecibles
|
||||
- Necesitas una ejecución determinista de herramientas
|
||||
- Quieres ejecutar una herramienta específica con parámetros conocidos
|
||||
- Estás construyendo flujos de trabajo estructurados con pasos predecibles
|
||||
|
||||
## Requisitos de permisos
|
||||
|
||||
@@ -99,22 +99,22 @@ La funcionalidad MCP requiere permisos específicos del espacio de trabajo:
|
||||
|--------|-------------------|
|
||||
| Configurar servidores MCP en ajustes | **Admin** |
|
||||
| Usar herramientas MCP en agentes | **Write** o **Admin** |
|
||||
| Ver herramientas MCP disponibles | **Read**, **Write**, o **Admin** |
|
||||
| Ver herramientas MCP disponibles | **Read**, **Write** o **Admin** |
|
||||
| Ejecutar bloques de Herramienta MCP | **Write** o **Admin** |
|
||||
|
||||
## Casos de uso comunes
|
||||
|
||||
### Integración con bases de datos
|
||||
Conéctate a bases de datos para consultar, insertar o actualizar datos dentro de tus flujos de trabajo.
|
||||
Conecta con bases de datos para consultar, insertar o actualizar datos dentro de tus flujos de trabajo.
|
||||
|
||||
### Integraciones de API
|
||||
Accede a APIs externas y servicios web que no tienen integraciones incorporadas en Sim.
|
||||
Accede a API externas y servicios web que no tienen integraciones integradas en Sim.
|
||||
|
||||
### Acceso al sistema de archivos
|
||||
Lee, escribe y manipula archivos en sistemas de archivos locales o remotos.
|
||||
|
||||
### Lógica de negocio personalizada
|
||||
Ejecuta scripts o herramientas personalizadas específicas para las necesidades de tu organización.
|
||||
Ejecuta scripts o herramientas personalizadas específicas de las necesidades de tu organización.
|
||||
|
||||
### Acceso a datos en tiempo real
|
||||
Obtén datos en vivo de sistemas externos durante la ejecución del flujo de trabajo.
|
||||
@@ -122,23 +122,23 @@ Obtén datos en vivo de sistemas externos durante la ejecución del flujo de tra
|
||||
## Consideraciones de seguridad
|
||||
|
||||
- Los servidores MCP se ejecutan con los permisos del usuario que los configuró
|
||||
- Verifica siempre las fuentes del servidor MCP antes de la instalación
|
||||
- Siempre verifica las fuentes de los servidores MCP antes de la instalación
|
||||
- Usa variables de entorno para datos de configuración sensibles
|
||||
- Revisa las capacidades del servidor MCP antes de conceder acceso a los agentes
|
||||
- Revisa las capacidades del servidor MCP antes de otorgar acceso a los agentes
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### El servidor MCP no aparece
|
||||
- Verifica que la configuración del servidor sea correcta
|
||||
- Comprueba que tienes los permisos necesarios
|
||||
- Asegúrate de que el servidor MCP esté en funcionamiento y sea accesible
|
||||
- Comprueba que tienes los permisos requeridos
|
||||
- Asegúrate de que el servidor MCP esté en ejecución y accesible
|
||||
|
||||
### Fallos en la ejecución de herramientas
|
||||
- Verifica que los parámetros de la herramienta estén correctamente formateados
|
||||
- Revisa los registros del servidor MCP para ver mensajes de error
|
||||
- Revisa los registros del servidor MCP para mensajes de error
|
||||
- Asegúrate de que la autenticación requerida esté configurada
|
||||
|
||||
### Errores de permisos
|
||||
- Confirma tu nivel de permisos en el espacio de trabajo
|
||||
- Comprueba si el servidor MCP requiere autenticación adicional
|
||||
- Verifica que el servidor esté configurado correctamente para tu espacio de trabajo
|
||||
- Confirma tu nivel de permisos del espacio de trabajo
|
||||
- Verifica si el servidor MCP requiere autenticación adicional
|
||||
- Comprueba que el servidor esté configurado correctamente para tu espacio de trabajo
|
||||
486
apps/docs/content/docs/es/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: Interactúa con Jira Service Management
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra con Jira Service Management para la gestión de servicios de TI. Crea y gestiona solicitudes de servicio, maneja clientes y organizaciones, realiza seguimiento de SLA y administra colas.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
Obtiene todos los service desks de Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `serviceDesks` | json | Array de service desks |
|
||||
| `total` | number | Número total de service desks |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
Obtiene los tipos de solicitud para un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del service desk para obtener tipos de solicitud |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `requestTypes` | json | Array de tipos de solicitud |
|
||||
| `total` | number | Número total de tipos de solicitud |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
Crear una nueva solicitud de servicio en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del Service Desk en el que crear la solicitud |
|
||||
| `requestTypeId` | string | Sí | ID del tipo de solicitud para la nueva solicitud |
|
||||
| `summary` | string | Sí | Resumen/título para la solicitud de servicio |
|
||||
| `description` | string | No | Descripción para la solicitud de servicio |
|
||||
| `raiseOnBehalfOf` | string | No | ID de cuenta del cliente para crear la solicitud en su nombre |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueId` | string | ID de la incidencia de solicitud creada |
|
||||
| `issueKey` | string | Clave de la incidencia de solicitud creada \(p. ej., SD-123\) |
|
||||
| `requestTypeId` | string | ID del tipo de solicitud |
|
||||
| `serviceDeskId` | string | ID del service desk |
|
||||
| `success` | boolean | Si la solicitud se creó correctamente |
|
||||
| `url` | string | URL de la solicitud creada |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
Obtener una única solicitud de servicio de Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
Obtener múltiples solicitudes de servicio de Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | No | Filtrar por ID de service desk |
|
||||
| `requestOwnership` | string | No | Filtrar por propiedad: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
|
||||
| `requestStatus` | string | No | Filtrar por estado: OPEN, CLOSED, ALL |
|
||||
| `searchTerm` | string | No | Término de búsqueda para filtrar solicitudes |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `requests` | json | Array de solicitudes de servicio |
|
||||
| `total` | number | Número total de solicitudes |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
Añadir un comentario (público o interno) a una solicitud de servicio en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
|
||||
| `body` | string | Sí | Texto del cuerpo del comentario |
|
||||
| `isPublic` | boolean | Sí | Si el comentario es público \(visible para el cliente\) o interno |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `commentId` | string | ID del comentario creado |
|
||||
| `body` | string | Texto del cuerpo del comentario |
|
||||
| `isPublic` | boolean | Si el comentario es público |
|
||||
| `success` | boolean | Si el comentario se añadió correctamente |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
Obtener comentarios de una solicitud de servicio en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
|
||||
| `isPublic` | boolean | No | Filtrar solo comentarios públicos |
|
||||
| `internal` | boolean | No | Filtrar solo comentarios internos |
|
||||
| `start` | number | No | Índice de inicio para la paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave del issue |
|
||||
| `comments` | json | Array de comentarios |
|
||||
| `total` | number | Número total de comentarios |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
Obtener clientes para un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del service desk para obtener clientes |
|
||||
| `query` | string | No | Consulta de búsqueda para filtrar clientes |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `customers` | json | Array de clientes |
|
||||
| `total` | number | Número total de clientes |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
Añadir clientes a un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del Service Desk al que añadir clientes |
|
||||
| `emails` | string | Sí | Direcciones de correo electrónico separadas por comas para añadir como clientes |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `serviceDeskId` | string | ID del service desk |
|
||||
| `success` | boolean | Si los clientes se añadieron correctamente |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
Obtener organizaciones de un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del Service Desk del que obtener organizaciones |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `organizations` | json | Array de organizaciones |
|
||||
| `total` | number | Número total de organizaciones |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
Crear una nueva organización en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `name` | string | Sí | Nombre de la organización a crear |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `organizationId` | string | ID de la organización creada |
|
||||
| `name` | string | Nombre de la organización creada |
|
||||
| `success` | boolean | Si la operación tuvo éxito |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
Añadir una organización a un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del service desk al que añadir la organización |
|
||||
| `organizationId` | string | Sí | ID de la organización a añadir al service desk |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `serviceDeskId` | string | ID del service desk |
|
||||
| `organizationId` | string | ID de la organización añadida |
|
||||
| `success` | boolean | Si la operación tuvo éxito |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
Obtener colas para un service desk en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `serviceDeskId` | string | Sí | ID del service desk para obtener colas |
|
||||
| `includeCount` | boolean | No | Incluir recuento de incidencias para cada cola |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `queues` | json | Array de colas |
|
||||
| `total` | number | Número total de colas |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
Obtener información de SLA para una solicitud de servicio en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `slas` | json | Array de información de SLA |
|
||||
| `total` | number | Número total de SLA |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
Obtener las transiciones disponibles para una solicitud de servicio en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `transitions` | json | Array de transiciones disponibles |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
Transicionar una solicitud de servicio a un nuevo estado en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
| `transitionId` | string | Sí | ID de transición a aplicar |
|
||||
| `comment` | string | No | Comentario opcional para añadir durante la transición |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `transitionId` | string | ID de transición aplicada |
|
||||
| `success` | boolean | Si la transición fue exitosa |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
Obtener participantes de una solicitud en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `participants` | json | Array de participantes |
|
||||
| `total` | number | Número total de participantes |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
Agregar participantes a una solicitud en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
| `accountIds` | string | Sí | IDs de cuenta separados por comas para agregar como participantes |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `participants` | json | Array de participantes añadidos |
|
||||
| `success` | boolean | Si la operación tuvo éxito |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
Obtener aprobaciones para una solicitud en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
| `start` | number | No | Índice de inicio para la paginación \(predeterminado: 0\) |
|
||||
| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `approvals` | json | Array de aprobaciones |
|
||||
| `total` | number | Número total de aprobaciones |
|
||||
| `isLastPage` | boolean | Si esta es la última página |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
Aprobar o rechazar una solicitud de aprobación en Jira Service Management
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
|
||||
| `cloudId` | string | No | ID de Jira Cloud para la instancia |
|
||||
| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
|
||||
| `approvalId` | string | Sí | ID de aprobación a responder |
|
||||
| `decision` | string | Sí | Decisión: "approve" o "decline" |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueIdOrKey` | string | ID o clave de la incidencia |
|
||||
| `approvalId` | string | ID de aprobación |
|
||||
| `decision` | string | Decisión tomada \(aprobar/rechazar\) |
|
||||
| `success` | boolean | Si la operación tuvo éxito |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
- Tipo: `jira_service_management`
|
||||
108
apps/docs/content/docs/fr/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Déployer des workflows en tant que MCP
|
||||
description: Exposez vos workflows en tant qu'outils MCP pour les assistants IA
|
||||
externes et les applications
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Déployez vos workflows en tant qu'outils MCP pour les rendre accessibles aux assistants IA externes comme Claude Desktop, Cursor et autres clients compatibles MCP. Cela transforme vos workflows en outils appelables qui peuvent être invoqués depuis n'importe où.
|
||||
|
||||
## Créer et gérer des serveurs MCP
|
||||
|
||||
Les serveurs MCP regroupent vos outils de workflow. Créez-les et gérez-les dans les paramètres de l'espace de travail :
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Accédez à **Paramètres → Serveurs MCP**
|
||||
2. Cliquez sur **Créer un serveur**
|
||||
3. Saisissez un nom et une description facultative
|
||||
4. Copiez l'URL du serveur pour l'utiliser dans vos clients MCP
|
||||
5. Consultez et gérez tous les outils ajoutés au serveur
|
||||
|
||||
## Ajouter un workflow en tant qu'outil
|
||||
|
||||
Une fois votre workflow déployé, vous pouvez l'exposer en tant qu'outil MCP :
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Ouvrez votre workflow déployé
|
||||
2. Cliquez sur **Déployer** et accédez à l'onglet **MCP**
|
||||
3. Configurez le nom et la description de l'outil
|
||||
4. Ajoutez des descriptions pour chaque paramètre (aide l'IA à comprendre les entrées)
|
||||
5. Sélectionnez les serveurs MCP auxquels l'ajouter
|
||||
|
||||
<Callout type="info">
|
||||
Le workflow doit être déployé avant de pouvoir être ajouté en tant qu'outil MCP.
|
||||
</Callout>
|
||||
|
||||
## Configuration de l'outil
|
||||
|
||||
### Nom de l'outil
|
||||
Utilisez des lettres minuscules, des chiffres et des traits de soulignement. Le nom doit être descriptif et suivre les conventions de nommage MCP (par exemple, `search_documents`, `send_email`).
|
||||
|
||||
### Description
|
||||
Rédigez une description claire de ce que fait l'outil. Cela aide les assistants IA à comprendre quand utiliser l'outil.
|
||||
|
||||
### Paramètres
|
||||
Les champs du format d'entrée de votre workflow deviennent des paramètres d'outil. Ajoutez des descriptions à chaque paramètre pour aider les assistants IA à fournir les valeurs correctes.
|
||||
|
||||
## Connexion des clients MCP
|
||||
|
||||
Utilisez l'URL du serveur depuis les paramètres pour connecter des applications externes :
|
||||
|
||||
### Claude Desktop
|
||||
Ajoutez à votre configuration Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`) :
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
Ajoutez l'URL du serveur dans les paramètres MCP de Cursor en utilisant le même modèle mcp-remote.
|
||||
|
||||
<Callout type="warn">
|
||||
Incluez votre en-tête de clé API (`X-API-Key`) pour un accès authentifié lors de l'utilisation de mcp-remote ou d'autres transports MCP basés sur HTTP.
|
||||
</Callout>
|
||||
|
||||
## Gestion du serveur
|
||||
|
||||
Depuis la vue détaillée du serveur dans **Paramètres → Serveurs MCP**, vous pouvez :
|
||||
|
||||
- **Voir les outils** : voir tous les workflows ajoutés à un serveur
|
||||
- **Copier l'URL** : obtenir l'URL du serveur pour les clients MCP
|
||||
- **Ajouter des workflows** : ajouter d'autres workflows déployés comme outils
|
||||
- **Supprimer des outils** : retirer des workflows du serveur
|
||||
- **Supprimer le serveur** : supprimer l'intégralité du serveur et tous ses outils
|
||||
|
||||
## Comment ça fonctionne
|
||||
|
||||
Lorsqu'un client MCP appelle votre outil :
|
||||
|
||||
1. La requête est reçue à l'URL de votre serveur MCP
|
||||
2. Sim valide la requête et mappe les paramètres aux entrées du workflow
|
||||
3. Le workflow déployé s'exécute avec les entrées fournies
|
||||
4. Les résultats sont renvoyés au client MCP
|
||||
|
||||
Les workflows s'exécutent en utilisant la même version de déploiement que les appels API, garantissant un comportement cohérent.
|
||||
|
||||
## Exigences de permission
|
||||
|
||||
| Action | Permission requise |
|
||||
|--------|-------------------|
|
||||
| Créer des serveurs MCP | **Admin** |
|
||||
| Ajouter des workflows aux serveurs | **Write** ou **Admin** |
|
||||
| Voir les serveurs MCP | **Read**, **Write** ou **Admin** |
|
||||
| Supprimer des serveurs MCP | **Admin** |
|
||||
@@ -1,8 +1,11 @@
|
||||
---
|
||||
title: MCP (Model Context Protocol)
|
||||
title: Utiliser les outils MCP
|
||||
description: Connectez des outils et services externes en utilisant le Model
|
||||
Context Protocol
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Le Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) vous permet de connecter des outils et services externes en utilisant un protocole standardisé, vous permettant d'intégrer des API et des services directement dans vos flux de travail. Avec MCP, vous pouvez étendre les capacités de Sim en ajoutant des intégrations personnalisées qui fonctionnent parfaitement avec vos agents et flux de travail.
|
||||
@@ -20,14 +23,8 @@ MCP est une norme ouverte qui permet aux assistants IA de se connecter de maniè
|
||||
|
||||
Les serveurs MCP fournissent des collections d'outils que vos agents peuvent utiliser. Configurez-les dans les paramètres de l'espace de travail :
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="Configuration du serveur MCP dans les paramètres"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. Accédez aux paramètres de votre espace de travail
|
||||
@@ -40,14 +37,18 @@ Les serveurs MCP fournissent des collections d'outils que vos agents peuvent uti
|
||||
Vous pouvez également configurer les serveurs MCP directement depuis la barre d'outils d'un bloc Agent pour une configuration rapide.
|
||||
</Callout>
|
||||
|
||||
## Utilisation des outils MCP dans les agents
|
||||
### Actualiser les outils
|
||||
|
||||
Une fois les serveurs MCP configurés, leurs outils deviennent disponibles dans vos blocs d'agents :
|
||||
Cliquez sur **Actualiser** sur un serveur pour récupérer les derniers schémas d'outils et mettre à jour automatiquement tous les blocs d'agent utilisant ces outils avec les nouvelles définitions de paramètres.
|
||||
|
||||
## Utiliser les outils MCP dans les agents
|
||||
|
||||
Une fois les serveurs MCP configurés, leurs outils deviennent disponibles dans vos blocs d'agent :
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-2.png"
|
||||
alt="Utilisation d'un outil MCP dans un bloc Agent"
|
||||
alt="Utilisation de l'outil MCP dans un bloc d'agent"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
@@ -61,7 +62,7 @@ Une fois les serveurs MCP configurés, leurs outils deviennent disponibles dans
|
||||
|
||||
## Bloc d'outil MCP autonome
|
||||
|
||||
Pour un contrôle plus précis, vous pouvez utiliser le bloc dédié d'outil MCP pour exécuter des outils MCP spécifiques :
|
||||
Pour un contrôle plus précis, vous pouvez utiliser le bloc d'outil MCP dédié pour exécuter des outils MCP spécifiques :
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -76,52 +77,52 @@ Pour un contrôle plus précis, vous pouvez utiliser le bloc dédié d'outil MCP
|
||||
Le bloc d'outil MCP vous permet de :
|
||||
- Exécuter directement n'importe quel outil MCP configuré
|
||||
- Transmettre des paramètres spécifiques à l'outil
|
||||
- Utiliser la sortie de l'outil dans les étapes suivantes du flux de travail
|
||||
- Enchaîner plusieurs outils MCP
|
||||
- Utiliser la sortie de l'outil dans les étapes suivantes du workflow
|
||||
- Enchaîner plusieurs outils MCP ensemble
|
||||
|
||||
### Quand utiliser l'outil MCP vs l'agent
|
||||
### Quand utiliser l'outil MCP ou l'agent
|
||||
|
||||
**Utilisez l'agent avec les outils MCP quand :**
|
||||
**Utilisez l'agent avec les outils MCP lorsque :**
|
||||
- Vous voulez que l'IA décide quels outils utiliser
|
||||
- Vous avez besoin d'un raisonnement complexe sur quand et comment utiliser les outils
|
||||
- Vous souhaitez une interaction en langage naturel avec les outils
|
||||
|
||||
**Utilisez le bloc Outil MCP quand :**
|
||||
- Vous avez besoin d'une exécution déterministe d'outils
|
||||
- Vous souhaitez exécuter un outil spécifique avec des paramètres connus
|
||||
- Vous construisez des flux de travail structurés avec des étapes prévisibles
|
||||
**Utilisez le bloc outil MCP quand :**
|
||||
- Vous avez besoin d'une exécution d'outil déterministe
|
||||
- Vous voulez exécuter un outil spécifique avec des paramètres connus
|
||||
- Vous construisez des workflows structurés avec des étapes prévisibles
|
||||
|
||||
## Exigences en matière d'autorisations
|
||||
## Exigences de permission
|
||||
|
||||
Les fonctionnalités MCP nécessitent des autorisations spécifiques pour l'espace de travail :
|
||||
La fonctionnalité MCP nécessite des permissions d'espace de travail spécifiques :
|
||||
|
||||
| Action | Autorisation requise |
|
||||
| Action | Permission requise |
|
||||
|--------|-------------------|
|
||||
| Configurer les serveurs MCP dans les paramètres | **Admin** |
|
||||
| Utiliser les outils MCP dans les agents | **Écriture** ou **Admin** |
|
||||
| Voir les outils MCP disponibles | **Lecture**, **Écriture**, ou **Admin** |
|
||||
| Exécuter des blocs d'Outil MCP | **Écriture** ou **Admin** |
|
||||
| Voir les outils MCP disponibles | **Lecture**, **Écriture** ou **Admin** |
|
||||
| Exécuter les blocs outil MCP | **Écriture** ou **Admin** |
|
||||
|
||||
## Cas d'utilisation courants
|
||||
## Cas d'usage courants
|
||||
|
||||
### Intégration de bases de données
|
||||
Connectez-vous aux bases de données pour interroger, insérer ou mettre à jour des données dans vos flux de travail.
|
||||
### Intégration de base de données
|
||||
Connectez-vous aux bases de données pour interroger, insérer ou mettre à jour des données dans vos workflows.
|
||||
|
||||
### Intégrations API
|
||||
Accédez à des API externes et des services web qui n'ont pas d'intégrations Sim intégrées.
|
||||
### Intégrations d'API
|
||||
Accédez aux API externes et services web qui n'ont pas d'intégrations Sim intégrées.
|
||||
|
||||
### Accès au système de fichiers
|
||||
Lisez, écrivez et manipulez des fichiers sur des systèmes de fichiers locaux ou distants.
|
||||
|
||||
### Logique métier personnalisée
|
||||
Exécutez des scripts ou des outils personnalisés spécifiques aux besoins de votre organisation.
|
||||
Exécutez des scripts ou outils personnalisés spécifiques aux besoins de votre organisation.
|
||||
|
||||
### Accès aux données en temps réel
|
||||
Récupérez des données en direct à partir de systèmes externes pendant l'exécution du flux de travail.
|
||||
Récupérez des données en direct depuis des systèmes externes pendant l'exécution du workflow.
|
||||
|
||||
## Considérations de sécurité
|
||||
|
||||
- Les serveurs MCP s'exécutent avec les autorisations de l'utilisateur qui les a configurés
|
||||
- Les serveurs MCP s'exécutent avec les permissions de l'utilisateur qui les a configurés
|
||||
- Vérifiez toujours les sources des serveurs MCP avant l'installation
|
||||
- Utilisez des variables d'environnement pour les données de configuration sensibles
|
||||
- Examinez les capacités du serveur MCP avant d'accorder l'accès aux agents
|
||||
@@ -130,10 +131,10 @@ Récupérez des données en direct à partir de systèmes externes pendant l'ex
|
||||
|
||||
### Le serveur MCP n'apparaît pas
|
||||
- Vérifiez que la configuration du serveur est correcte
|
||||
- Vérifiez que vous disposez des autorisations requises
|
||||
- Vérifiez que vous avez les permissions requises
|
||||
- Assurez-vous que le serveur MCP est en cours d'exécution et accessible
|
||||
|
||||
### Échecs d'exécution d'outils
|
||||
### Échecs d'exécution d'outil
|
||||
- Vérifiez que les paramètres de l'outil sont correctement formatés
|
||||
- Consultez les journaux du serveur MCP pour les messages d'erreur
|
||||
- Assurez-vous que l'authentification requise est configurée
|
||||
|
||||
486
apps/docs/content/docs/fr/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: Interagir avec Jira Service Management
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez-vous avec Jira Service Management pour la gestion des services informatiques. Créez et gérez des demandes de service, traitez les clients et les organisations, suivez les SLA et gérez les files d'attente.
|
||||
|
||||
## Outils
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
Obtenir tous les centres de services depuis Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `serviceDesks` | json | Tableau des centres de services |
|
||||
| `total` | number | Nombre total de centres de services |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
Obtenir les types de demandes pour un centre de services dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du centre de services pour lequel obtenir les types de demandes |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `requestTypes` | json | Tableau des types de demande |
|
||||
| `total` | number | Nombre total de types de demande |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
Créer une nouvelle demande de service dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du Service Desk dans lequel créer la demande |
|
||||
| `requestTypeId` | string | Oui | ID du type de demande pour la nouvelle demande |
|
||||
| `summary` | string | Oui | Résumé/titre de la demande de service |
|
||||
| `description` | string | Non | Description de la demande de service |
|
||||
| `raiseOnBehalfOf` | string | Non | ID de compte du client pour lequel créer la demande |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueId` | string | ID du ticket de demande créé |
|
||||
| `issueKey` | string | Clé du ticket de demande créé \(par exemple, SD-123\) |
|
||||
| `requestTypeId` | string | ID du type de demande |
|
||||
| `serviceDeskId` | string | ID du Service Desk |
|
||||
| `success` | boolean | Indique si la demande a été créée avec succès |
|
||||
| `url` | string | URL vers la demande créée |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
Obtenir une seule demande de service depuis Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
Obtenir plusieurs demandes de service depuis Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Non | Filtrer par ID de service desk |
|
||||
| `requestOwnership` | string | Non | Filtrer par propriété : OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
|
||||
| `requestStatus` | string | Non | Filtrer par statut : OPEN, CLOSED, ALL |
|
||||
| `searchTerm` | string | Non | Terme de recherche pour filtrer les demandes |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `requests` | json | Tableau des demandes de service |
|
||||
| `total` | number | Nombre total de demandes |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
Ajouter un commentaire (public ou interne) à une demande de service dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
|
||||
| `body` | string | Oui | Texte du corps du commentaire |
|
||||
| `isPublic` | boolean | Oui | Indique si le commentaire est public \(visible par le client\) ou interne |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `commentId` | string | ID du commentaire créé |
|
||||
| `body` | string | Texte du corps du commentaire |
|
||||
| `isPublic` | boolean | Indique si le commentaire est public |
|
||||
| `success` | boolean | Indique si le commentaire a été ajouté avec succès |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
Obtenir les commentaires d'une demande de service dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
|
||||
| `isPublic` | boolean | Non | Filtrer uniquement les commentaires publics |
|
||||
| `internal` | boolean | Non | Filtrer uniquement les commentaires internes |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `comments` | json | Tableau des commentaires |
|
||||
| `total` | number | Nombre total de commentaires |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
Obtenir les clients d'un service desk dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Jira Cloud de l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du service desk pour lequel obtenir les clients |
|
||||
| `query` | string | Non | Requête de recherche pour filtrer les clients |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `customers` | json | Tableau des clients |
|
||||
| `total` | number | Nombre total de clients |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
Ajouter des clients à un service desk dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du Service Desk auquel ajouter des clients |
|
||||
| `emails` | string | Oui | Adresses e-mail séparées par des virgules à ajouter comme clients |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `serviceDeskId` | string | ID du Service Desk |
|
||||
| `success` | boolean | Indique si les clients ont été ajoutés avec succès |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
Obtenir les organisations d'un Service Desk dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du Service Desk pour lequel obtenir les organisations |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `organizations` | json | Tableau des organisations |
|
||||
| `total` | number | Nombre total d'organisations |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
Créer une nouvelle organisation dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `name` | string | Oui | Nom de l'organisation à créer |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `organizationId` | string | ID de l'organisation créée |
|
||||
| `name` | string | Nom de l'organisation créée |
|
||||
| `success` | boolean | Indique si l'opération a réussi |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
Ajouter une organisation à un service desk dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du service desk auquel ajouter l'organisation |
|
||||
| `organizationId` | string | Oui | ID de l'organisation à ajouter au service desk |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `serviceDeskId` | string | ID du service desk |
|
||||
| `organizationId` | string | ID de l'organisation ajoutée |
|
||||
| `success` | boolean | Indique si l'opération a réussi |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
Obtenir les files d'attente pour un service desk dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par ex., votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `serviceDeskId` | string | Oui | ID du service desk pour lequel obtenir les files d'attente |
|
||||
| `includeCount` | boolean | Non | Inclure le nombre de tickets pour chaque file d'attente |
|
||||
| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `queues` | json | Tableau des files d'attente |
|
||||
| `total` | number | Nombre total de files d'attente |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
Obtenir les informations SLA pour une demande de service dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par ex., votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par ex., SD-123) |
|
||||
| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `slas` | json | Tableau des informations SLA |
|
||||
| `total` | number | Nombre total de SLA |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
Obtenir les transitions disponibles pour une demande de service dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Jira Cloud de l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `transitions` | json | Tableau des transitions disponibles |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
Faire passer une demande de service à un nouveau statut dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Jira Cloud de l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
|
||||
| `transitionId` | string | Oui | ID de la transition à appliquer |
|
||||
| `comment` | string | Non | Commentaire optionnel à ajouter lors de la transition |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `transitionId` | string | ID de la transition appliquée |
|
||||
| `success` | boolean | Indique si la transition a réussi |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
Obtenir les participants d'une demande dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira de l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
|
||||
| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `participants` | json | Tableau des participants |
|
||||
| `total` | number | Nombre total de participants |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
Ajouter des participants à une demande dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira de l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
|
||||
| `accountIds` | string | Oui | ID de comptes séparés par des virgules à ajouter comme participants |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `participants` | json | Tableau des participants ajoutés |
|
||||
| `success` | boolean | Indique si l'opération a réussi |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
Obtenir les approbations pour une demande dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
|
||||
| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
|
||||
| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `approvals` | json | Tableau des approbations |
|
||||
| `total` | number | Nombre total d'approbations |
|
||||
| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
Approuver ou refuser une demande d'approbation dans Jira Service Management
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
|
||||
| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
|
||||
| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
|
||||
| `approvalId` | string | Oui | ID de l'approbation à traiter |
|
||||
| `decision` | string | Oui | Décision : "approve" ou "decline" |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueIdOrKey` | string | ID ou clé du ticket |
|
||||
| `approvalId` | string | ID d'approbation |
|
||||
| `decision` | string | Décision prise \(approuver/refuser\) |
|
||||
| `success` | boolean | Indique si l'opération a réussi |
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `tools`
|
||||
- Type : `jira_service_management`
|
||||
107
apps/docs/content/docs/ja/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: ワークフローをMCPとしてデプロイ
|
||||
description: 外部のAIアシスタントやアプリケーション向けに、ワークフローをMCPツールとして公開
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
ワークフローをMCPツールとしてデプロイすることで、Claude Desktop、Cursor、その他のMCP互換クライアントなどの外部AIアシスタントからアクセス可能になります。これにより、ワークフローがどこからでも呼び出せる呼び出し可能なツールに変わります。
|
||||
|
||||
## MCPサーバーの作成と管理
|
||||
|
||||
MCPサーバーは、ワークフローツールをまとめてグループ化します。ワークスペース設定で作成と管理を行います。
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. **設定 → MCPサーバー**に移動
|
||||
2. **サーバーを作成**をクリック
|
||||
3. 名前と説明(任意)を入力
|
||||
4. MCPクライアントで使用するためにサーバーURLをコピー
|
||||
5. サーバーに追加されたすべてのツールを表示・管理
|
||||
|
||||
## ワークフローをツールとして追加
|
||||
|
||||
ワークフローがデプロイされたら、MCPツールとして公開できます。
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. デプロイ済みのワークフローを開く
|
||||
2. **デプロイ**をクリックし、**MCP**タブに移動
|
||||
3. ツール名と説明を設定
|
||||
4. 各パラメータの説明を追加(AIが入力を理解するのに役立ちます)
|
||||
5. 追加先のMCPサーバーを選択
|
||||
|
||||
<Callout type="info">
|
||||
ワークフローをMCPツールとして追加する前に、デプロイしておく必要があります。
|
||||
</Callout>
|
||||
|
||||
## ツールの設定
|
||||
|
||||
### ツール名
|
||||
小文字、数字、アンダースコアを使用します。名前は説明的で、MCPの命名規則に従う必要があります(例: `search_documents`、`send_email`)。
|
||||
|
||||
### 説明
|
||||
ツールが何をするのかを明確に説明します。これにより、AIアシスタントがツールをいつ使用すべきかを理解できます。
|
||||
|
||||
### パラメータ
|
||||
ワークフローの入力形式フィールドがツールパラメータになります。AIアシスタントが正しい値を提供できるよう、各パラメータに説明を追加してください。
|
||||
|
||||
## MCPクライアントの接続
|
||||
|
||||
設定から取得したサーバーURLを使用して外部アプリケーションを接続します:
|
||||
|
||||
### Claude Desktop
|
||||
Claude Desktopの設定ファイル(`~/Library/Application Support/Claude/claude_desktop_config.json`)に追加してください:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
同じmcp-remoteパターンを使用して、CursorのMCP設定にサーバーURLを追加してください。
|
||||
|
||||
<Callout type="warn">
|
||||
mcp-remoteまたは他のHTTPベースのMCPトランスポートを使用する際は、認証アクセスのためにAPIキーヘッダー(`X-API-Key`)を含めてください。
|
||||
</Callout>
|
||||
|
||||
## サーバー管理
|
||||
|
||||
**設定 → MCPサーバー**のサーバー詳細ビューから、以下の操作が可能です:
|
||||
|
||||
- **ツールを表示**: サーバーに追加されたすべてのワークフローを確認
|
||||
- **URLをコピー**: MCPクライアント用のサーバーURLを取得
|
||||
- **ワークフローを追加**: デプロイ済みワークフローをツールとして追加
|
||||
- **ツールを削除**: サーバーからワークフローを削除
|
||||
- **サーバーを削除**: サーバー全体とすべてのツールを削除
|
||||
|
||||
## 仕組み
|
||||
|
||||
MCPクライアントがツールを呼び出すと:
|
||||
|
||||
1. MCPサーバーURLでリクエストを受信
|
||||
2. Simがリクエストを検証し、パラメータをワークフロー入力にマッピング
|
||||
3. 提供された入力でデプロイ済みワークフローを実行
|
||||
4. 結果をMCPクライアントに返却
|
||||
|
||||
ワークフローはAPI呼び出しと同じデプロイバージョンを使用して実行されるため、一貫した動作が保証されます。
|
||||
|
||||
## 必要な権限
|
||||
|
||||
| アクション | 必要な権限 |
|
||||
|--------|-------------------|
|
||||
| MCPサーバーを作成 | **管理者** |
|
||||
| サーバーにワークフローを追加 | **書き込み**または**管理者** |
|
||||
| MCPサーバーを表示 | **読み取り**、**書き込み**、または**管理者** |
|
||||
| MCPサーバーを削除 | **管理者** |
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: MCP(モデルコンテキストプロトコル)
|
||||
title: MCPツールの使用
|
||||
description: Model Context Protocolを使用して外部ツールとサービスを接続
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
モデルコンテキストプロトコル([MCP](https://modelcontextprotocol.com/))を使用すると、標準化されたプロトコルを使用して外部ツールやサービスを接続し、APIやサービスをワークフローに直接統合することができます。MCPを使用することで、エージェントやワークフローとシームレスに連携するカスタム統合機能を追加して、Simの機能を拡張できます。
|
||||
@@ -20,14 +22,8 @@ MCPは、AIアシスタントが外部データソースやツールに安全に
|
||||
|
||||
MCPサーバーはエージェントが使用できるツールのコレクションを提供します。ワークスペース設定で構成してください:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="設定でのMCPサーバーの構成"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. ワークスペース設定に移動します
|
||||
@@ -40,9 +36,13 @@ MCPサーバーはエージェントが使用できるツールのコレクシ
|
||||
エージェントブロックのツールバーから直接MCPサーバーを構成することもできます(クイックセットアップ)。
|
||||
</Callout>
|
||||
|
||||
### ツールの更新
|
||||
|
||||
サーバーの**更新**をクリックすると、最新のツールスキーマを取得し、それらのツールを使用しているエージェントブロックを新しいパラメータ定義で自動的に更新します。
|
||||
|
||||
## エージェントでのMCPツールの使用
|
||||
|
||||
MCPサーバーが構成されると、そのツールはエージェントブロック内で利用可能になります:
|
||||
MCPサーバーが設定されると、そのツールがエージェントブロック内で利用可能になります:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -54,14 +54,14 @@ MCPサーバーが構成されると、そのツールはエージェントブ
|
||||
/>
|
||||
</div>
|
||||
|
||||
1. **エージェント**ブロックを開きます
|
||||
1. **エージェント**ブロックを開く
|
||||
2. **ツール**セクションで、利用可能なMCPツールが表示されます
|
||||
3. エージェントに使用させたいツールを選択します
|
||||
4. これでエージェントは実行中にこれらのツールにアクセスできるようになります
|
||||
3. エージェントに使用させたいツールを選択
|
||||
4. エージェントは実行中にこれらのツールにアクセスできるようになります
|
||||
|
||||
## スタンドアロンMCPツールブロック
|
||||
|
||||
より細かい制御のために、特定のMCPツールを実行するための専用MCPツールブロックを使用できます:
|
||||
より細かい制御が必要な場合は、専用のMCPツールブロックを使用して特定のMCPツールを実行できます:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -73,18 +73,18 @@ MCPサーバーが構成されると、そのツールはエージェントブ
|
||||
/>
|
||||
</div>
|
||||
|
||||
MCPツールブロックでは以下のことが可能です:
|
||||
- 構成済みのMCPツールを直接実行する
|
||||
MCPツールブロックでは次のことができます:
|
||||
- 設定済みのMCPツールを直接実行
|
||||
- ツールに特定のパラメータを渡す
|
||||
- ツールの出力を後続のワークフローステップで使用する
|
||||
- ツールの出力を後続のワークフローステップで使用
|
||||
- 複数のMCPツールを連鎖させる
|
||||
|
||||
### MCPツールとエージェントの使い分け
|
||||
|
||||
**エージェントとMCPツールを使用する場合:**
|
||||
- AIにどのツールを使用するか決定させたい場合
|
||||
- ツールをいつどのように使用するかについて複雑な推論が必要な場合
|
||||
- ツールと自然言語でのやり取りが必要な場合
|
||||
**MCPツールを使用したエージェントを使用する場合:**
|
||||
- AIにどのツールを使用するか決定させたい
|
||||
- ツールをいつどのように使用するかについて複雑な推論が必要
|
||||
- ツールとの自然言語による対話が必要
|
||||
|
||||
**MCPツールブロックを使用する場合:**
|
||||
- 決定論的なツール実行が必要な場合
|
||||
@@ -97,34 +97,34 @@ MCP機能には特定のワークスペース権限が必要です:
|
||||
|
||||
| アクション | 必要な権限 |
|
||||
|--------|-------------------|
|
||||
| 設定でMCPサーバーを構成する | **管理者** |
|
||||
| エージェントでMCPツールを使用する | **書き込み** または **管理者** |
|
||||
| 利用可能なMCPツールを表示する | **読み取り**、**書き込み**、または **管理者** |
|
||||
| MCPツールブロックを実行する | **書き込み** または **管理者** |
|
||||
| 設定でMCPサーバーを構成 | **管理者** |
|
||||
| エージェントでMCPツールを使用 | **書き込み**または**管理者** |
|
||||
| 利用可能なMCPツールを表示 | **読み取り**、**書き込み**、または**管理者** |
|
||||
| MCPツールブロックを実行 | **書き込み**または**管理者** |
|
||||
|
||||
## 一般的なユースケース
|
||||
## 一般的な使用例
|
||||
|
||||
### データベース統合
|
||||
ワークフロー内でデータのクエリ、挿入、更新を行うためにデータベースに接続します。
|
||||
データベースに接続して、ワークフロー内でデータのクエリ、挿入、更新を行います。
|
||||
|
||||
### API統合
|
||||
組み込みのSim統合がない外部APIやWebサービスにアクセスします。
|
||||
Simに組み込まれていない外部APIやWebサービスにアクセスします。
|
||||
|
||||
### ファイルシステムアクセス
|
||||
ローカルまたはリモートファイルシステム上のファイルの読み取り、書き込み、操作を行います。
|
||||
ローカルまたはリモートのファイルシステム上のファイルの読み取り、書き込み、操作を行います。
|
||||
|
||||
### カスタムビジネスロジック
|
||||
組織のニーズに特化したカスタムスクリプトやツールを実行します。
|
||||
組織固有のニーズに合わせたカスタムスクリプトやツールを実行します。
|
||||
|
||||
### リアルタイムデータアクセス
|
||||
ワークフロー実行中に外部システムからライブデータを取得します。
|
||||
|
||||
## セキュリティに関する考慮事項
|
||||
|
||||
- MCPサーバーは構成したユーザーの権限で実行されます
|
||||
- MCPサーバーは、それを構成したユーザーの権限で実行されます
|
||||
- インストール前に必ずMCPサーバーのソースを確認してください
|
||||
- 機密性の高い構成データには環境変数を使用してください
|
||||
- エージェントにアクセス権を付与する前にMCPサーバーの機能を確認してください
|
||||
- エージェントにアクセスを許可する前にMCPサーバーの機能を確認してください
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
@@ -139,6 +139,6 @@ MCP機能には特定のワークスペース権限が必要です:
|
||||
- 必要な認証が構成されていることを確認してください
|
||||
|
||||
### 権限エラー
|
||||
- ワークスペースの権限レベルを確認する
|
||||
- MCPサーバーが追加認証を必要としているか確認する
|
||||
- サーバーがワークスペース用に適切に構成されているか確認する
|
||||
- ワークスペースの権限レベルを確認してください
|
||||
- MCPサーバーが追加の認証を必要としているか確認してください
|
||||
- サーバーがワークスペースに対して適切に設定されているか確認してください
|
||||
486
apps/docs/content/docs/ja/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: Jira Service Managementと連携する
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## 使用方法
|
||||
|
||||
ITサービス管理のためにJira Service Managementと統合します。サービスリクエストの作成と管理、顧客と組織の処理、SLAの追跡、キューの管理を行います。
|
||||
|
||||
## ツール
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
Jira Service Managementからすべてのサービスデスクを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `serviceDesks` | json | サービスデスクの配列 |
|
||||
| `total` | number | サービスデスクの総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
Jira Service Managementのサービスデスクのリクエストタイプを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | リクエストタイプを取得するサービスデスクID |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `requestTypes` | json | リクエストタイプの配列 |
|
||||
| `total` | number | リクエストタイプの総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
Jira Service Managementで新しいサービスリクエストを作成
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | リクエストを作成するサービスデスクID |
|
||||
| `requestTypeId` | string | はい | 新しいリクエストのリクエストタイプID |
|
||||
| `summary` | string | はい | サービスリクエストの概要/タイトル |
|
||||
| `description` | string | いいえ | サービスリクエストの説明 |
|
||||
| `raiseOnBehalfOf` | string | いいえ | 代理でリクエストを作成する顧客のアカウントID |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueId` | string | 作成されたリクエストの課題ID |
|
||||
| `issueKey` | string | 作成されたリクエストの課題キー(例:SD-123) |
|
||||
| `requestTypeId` | string | リクエストタイプID |
|
||||
| `serviceDeskId` | string | サービスデスクID |
|
||||
| `success` | boolean | リクエストが正常に作成されたかどうか |
|
||||
| `url` | string | 作成されたリクエストのURL |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
Jira Service Managementから単一のサービスリクエストを取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
Jira Service Managementから複数のサービスリクエストを取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | いいえ | サービスデスクIDでフィルタ |
|
||||
| `requestOwnership` | string | いいえ | 所有権でフィルタ:OWNED_REQUESTS、PARTICIPATED_REQUESTS、ORGANIZATION、ALL_REQUESTS |
|
||||
| `requestStatus` | string | いいえ | ステータスでフィルタ:OPEN、CLOSED、ALL |
|
||||
| `searchTerm` | string | いいえ | リクエストをフィルタする検索語 |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `requests` | json | サービスリクエストの配列 |
|
||||
| `total` | number | リクエストの総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
Jira Service Managementのサービスリクエストにコメント(公開または内部)を追加する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
| `body` | string | はい | コメント本文 |
|
||||
| `isPublic` | boolean | はい | コメントが公開(顧客に表示)か内部かを指定 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `commentId` | string | 作成されたコメントID |
|
||||
| `body` | string | コメント本文 |
|
||||
| `isPublic` | boolean | コメントが公開かどうか |
|
||||
| `success` | boolean | コメントが正常に追加されたかどうか |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
Jira Service Managementのサービスリクエストのコメントを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
| `isPublic` | boolean | いいえ | 公開コメントのみにフィルタ |
|
||||
| `internal` | boolean | いいえ | 内部コメントのみにフィルタ |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `comments` | json | コメントの配列 |
|
||||
| `total` | number | コメントの総数 |
|
||||
| `isLastPage` | boolean | 最後のページかどうか |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
Jira Service Managementのサービスデスクの顧客を取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | 顧客を取得するサービスデスクID |
|
||||
| `query` | string | いいえ | 顧客をフィルタリングする検索クエリ |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト: 0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト: 50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `customers` | json | 顧客の配列 |
|
||||
| `total` | number | 顧客の総数 |
|
||||
| `isLastPage` | boolean | 最後のページかどうか |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
Jira Service Managementのサービスデスクに顧客を追加
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | 顧客を追加するサービスデスクID |
|
||||
| `emails` | string | はい | 顧客として追加するメールアドレス(カンマ区切り) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `serviceDeskId` | string | サービスデスクID |
|
||||
| `success` | boolean | 顧客が正常に追加されたかどうか |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
Jira Service Managementのサービスデスクの組織を取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | 組織を取得するサービスデスクID |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `organizations` | json | 組織の配列 |
|
||||
| `total` | number | 組織の総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
Jira Service Managementで新しい組織を作成する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `name` | string | はい | 作成する組織の名前 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `organizationId` | string | 作成された組織のID |
|
||||
| `name` | string | 作成された組織の名前 |
|
||||
| `success` | boolean | 操作が成功したかどうか |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
Jira Service Managementのサービスデスクに組織を追加する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | 組織を追加するサービスデスクID |
|
||||
| `organizationId` | string | はい | サービスデスクに追加する組織ID |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `serviceDeskId` | string | サービスデスクID |
|
||||
| `organizationId` | string | 追加された組織ID |
|
||||
| `success` | boolean | 操作が成功したかどうか |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
Jira Service Managementでサービスデスクのキューを取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `serviceDeskId` | string | はい | キューを取得するサービスデスクID |
|
||||
| `includeCount` | boolean | いいえ | 各キューの課題数を含める |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `queues` | json | キューの配列 |
|
||||
| `total` | number | キューの総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
Jira Service ManagementでサービスリクエストのSLA情報を取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `slas` | json | SLA情報の配列 |
|
||||
| `total` | number | SLAの総数 |
|
||||
| `isLastPage` | boolean | 最後のページかどうか |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
Jira Service Managementのサービスリクエストで利用可能なトランジションを取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `transitions` | json | 利用可能なトランジションの配列 |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
Jira Service Managementでサービスリクエストを新しいステータスにトランジション
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
|
||||
| `transitionId` | string | はい | 適用するトランジションID |
|
||||
| `comment` | string | いいえ | トランジション時に追加するオプションのコメント |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `transitionId` | string | 適用されたトランジションID |
|
||||
| `success` | boolean | トランジションが成功したかどうか |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
Jira Service Managementのリクエストの参加者を取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト: 0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト: 50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `participants` | json | 参加者の配列 |
|
||||
| `total` | number | 参加者の総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
Jira Service Managementのリクエストに参加者を追加
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
|
||||
| `accountIds` | string | はい | 参加者として追加するアカウントIDのカンマ区切りリスト |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `participants` | json | 追加された参加者の配列 |
|
||||
| `success` | boolean | 操作が成功したかどうか |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
Jira Service Managementでリクエストの承認を取得
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
|
||||
| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `approvals` | json | 承認の配列 |
|
||||
| `total` | number | 承認の総数 |
|
||||
| `isLastPage` | boolean | これが最後のページかどうか |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
Jira Service Managementで承認リクエストを承認または却下
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
|
||||
| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
|
||||
| `approvalId` | string | はい | 回答する承認ID |
|
||||
| `decision` | string | はい | 決定:「approve」または「decline」 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueIdOrKey` | string | 課題IDまたはキー |
|
||||
| `approvalId` | string | 承認ID |
|
||||
| `decision` | string | 決定内容(承認/却下) |
|
||||
| `success` | boolean | 操作が成功したかどうか |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリ: `tools`
|
||||
- タイプ: `jira_service_management`
|
||||
107
apps/docs/content/docs/zh/mcp/deploy-workflows.mdx
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: 将工作流部署为 MCP
|
||||
description: 将您的工作流公开为 MCP 工具,供外部 AI 助手和应用程序使用
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
将您的工作流部署为 MCP 工具,使其可供外部 AI 助手(如 Claude Desktop、Cursor 以及其他兼容 MCP 的客户端)访问。这会让您的工作流变成可随时调用的工具。
|
||||
|
||||
## 创建和管理 MCP 服务器
|
||||
|
||||
MCP 服务器用于将您的工作流工具进行分组。您可以在工作区设置中创建和管理这些服务器:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. 进入 **设置 → MCP 服务器**
|
||||
2. 点击 **创建服务器**
|
||||
3. 输入名称和可选描述
|
||||
4. 复制服务器 URL 以在您的 MCP 客户端中使用
|
||||
5. 查看并管理已添加到服务器的所有工具
|
||||
|
||||
## 添加工作流为工具
|
||||
|
||||
当您的工作流部署完成后,可以将其公开为 MCP 工具:
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. 打开已部署的工作流
|
||||
2. 点击 **部署** 并进入 **MCP** 标签页
|
||||
3. 配置工具名称和描述
|
||||
4. 为每个参数添加描述(帮助 AI 理解输入)
|
||||
5. 选择要添加到的 MCP 服务器
|
||||
|
||||
<Callout type="info">
|
||||
工作流必须先部署,才能添加为 MCP 工具。
|
||||
</Callout>
|
||||
|
||||
## 工具配置
|
||||
|
||||
### 工具名称
|
||||
请使用小写字母、数字和下划线。名称应具有描述性,并遵循 MCP 命名规范(如 `search_documents`、`send_email`)。
|
||||
|
||||
### 描述
|
||||
请清晰描述该工具的功能。这有助于 AI 助手理解何时使用该工具。
|
||||
|
||||
### 参数
|
||||
您的工作流输入格式字段会变成工具参数。为每个参数添加描述,有助于 AI 助手提供正确的值。
|
||||
|
||||
## 连接 MCP 客户端
|
||||
|
||||
使用设置中的服务器 URL 连接外部应用程序:
|
||||
|
||||
### Claude Desktop
|
||||
将以下内容添加到您的 Claude Desktop 配置中(`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-sim-workflows": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
在 Cursor 的 MCP 设置中,使用相同的 mcp-remote 格式添加服务器 URL。
|
||||
|
||||
<Callout type="warn">
|
||||
使用 mcp-remote 或其他基于 HTTP 的 MCP 传输方式时,请包含 API key header(`X-API-Key`)以进行身份验证访问。
|
||||
</Callout>
|
||||
|
||||
## 服务器管理
|
||||
|
||||
在 **设置 → MCP 服务器** 的服务器详情视图中,您可以:
|
||||
|
||||
- **查看工具**:查看添加到服务器的所有工作流
|
||||
- **复制 URL**:获取 MCP 客户端的服务器 URL
|
||||
- **添加工作流**:将更多已部署的工作流添加为工具
|
||||
- **移除工具**:从服务器中移除工作流
|
||||
- **删除服务器**:移除整个服务器及其所有工具
|
||||
|
||||
## 工作原理
|
||||
|
||||
当 MCP 客户端调用您的工具时:
|
||||
|
||||
1. 请求会发送到您的 MCP 服务器 URL
|
||||
2. Sim 验证请求并将参数映射到工作流输入
|
||||
3. 已部署的工作流会使用提供的输入执行
|
||||
4. 结果返回给 MCP 客户端
|
||||
|
||||
工作流执行时使用与 API 调用相同的部署版本,确保行为一致。
|
||||
|
||||
## 权限要求
|
||||
|
||||
| 操作 | 所需权限 |
|
||||
|--------|-------------------|
|
||||
| 创建 MCP 服务器 | **Admin** |
|
||||
| 向服务器添加工作流 | **Write** 或 **Admin** |
|
||||
| 查看 MCP 服务器 | **Read**、**Write** 或 **Admin** |
|
||||
| 删除 MCP 服务器 | **Admin** |
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
title: MCP(模型上下文协议)
|
||||
title: 使用 MCP 工具
|
||||
description: 通过 Model Context Protocol 连接外部工具和服务
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
模型上下文协议([MCP](https://modelcontextprotocol.com/))允许您使用标准化协议连接外部工具和服务,从而将 API 和服务直接集成到您的工作流程中。通过 MCP,您可以通过添加自定义集成来扩展 Sim 的功能,使其与您的代理和工作流程无缝协作。
|
||||
@@ -20,14 +22,8 @@ MCP 是一项开放标准,使 AI 助手能够安全地连接到外部数据源
|
||||
|
||||
MCP 服务器提供工具集合,供您的代理使用。您可以在工作区设置中进行配置:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-1.png"
|
||||
alt="在设置中配置 MCP 服务器"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
|
||||
</div>
|
||||
|
||||
1. 进入您的工作区设置
|
||||
@@ -40,56 +36,60 @@ MCP 服务器提供工具集合,供您的代理使用。您可以在工作区
|
||||
您还可以直接从代理模块的工具栏中配置 MCP 服务器,以便快速设置。
|
||||
</Callout>
|
||||
|
||||
## 在代理中使用 MCP 工具
|
||||
### 刷新工具
|
||||
|
||||
一旦配置了 MCP 服务器,其工具将在您的代理模块中可用:
|
||||
点击服务器上的 **刷新**,即可获取最新的工具 schema,并自动用新的参数定义更新所有使用这些工具的 agent 模块。
|
||||
|
||||
## 在 Agent 中使用 MCP 工具
|
||||
|
||||
配置好 MCP 服务器后,其工具会在你的 agent 模块中可用:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-2.png"
|
||||
alt="在代理模块中使用 MCP 工具"
|
||||
alt="在 Agent 模块中使用 MCP 工具"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
1. 打开一个 **代理** 模块
|
||||
2. 在 **工具** 部分,您将看到可用的 MCP 工具
|
||||
3. 选择您希望代理使用的工具
|
||||
4. 代理现在可以在执行过程中访问这些工具
|
||||
1. 打开一个 **Agent** 模块
|
||||
2. 在 **工具** 部分,你会看到可用的 MCP 工具
|
||||
3. 选择你希望 agent 使用的工具
|
||||
4. agent 在执行时即可访问这些工具
|
||||
|
||||
## 独立的 MCP 工具模块
|
||||
## 独立 MCP 工具模块
|
||||
|
||||
为了更精细的控制,您可以使用专用的 MCP 工具模块来执行特定的 MCP 工具:
|
||||
如需更细致的控制,可以使用专用的 MCP 工具模块来执行特定的 MCP 工具:
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/mcp-3.png"
|
||||
alt="独立的 MCP 工具模块"
|
||||
alt="独立 MCP 工具模块"
|
||||
width={700}
|
||||
height={450}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
MCP 工具模块允许您:
|
||||
- 直接执行任何已配置的 MCP 工具
|
||||
MCP 工具模块可以让你:
|
||||
- 直接执行任意已配置的 MCP 工具
|
||||
- 向工具传递特定参数
|
||||
- 在后续工作流步骤中使用工具的输出
|
||||
- 将多个 MCP 工具串联在一起
|
||||
- 在后续工作流步骤中使用工具输出
|
||||
- 串联多个 MCP 工具
|
||||
|
||||
### 何时使用 MCP 工具与代理
|
||||
### 何时使用 MCP 工具模块与 Agent
|
||||
|
||||
**在以下情况下使用带有 MCP 工具的代理:**
|
||||
- 您希望 AI 决定使用哪些工具
|
||||
- 您需要复杂的推理来决定何时以及如何使用工具
|
||||
- 您希望与工具进行自然语言交互
|
||||
**当你需要以下场景时,使用 Agent 搭配 MCP 工具:**
|
||||
- 希望 AI 决定使用哪些工具
|
||||
- 需要复杂推理来判断何时及如何使用工具
|
||||
- 希望通过自然语言与工具交互
|
||||
|
||||
**在以下情况下使用 MCP 工具块:**
|
||||
- 您需要确定性的工具执行
|
||||
- 您希望使用已知参数执行特定工具
|
||||
- 您正在构建具有可预测步骤的结构化工作流
|
||||
- 你需要确定性的工具执行
|
||||
- 你想用已知参数执行特定工具
|
||||
- 你正在构建具有可预测步骤的结构化工作流
|
||||
|
||||
## 权限要求
|
||||
|
||||
@@ -97,48 +97,48 @@ MCP 功能需要特定的工作区权限:
|
||||
|
||||
| 操作 | 所需权限 |
|
||||
|--------|-------------------|
|
||||
| 在设置中配置 MCP 服务器 | **管理员** |
|
||||
| 在代理中使用 MCP 工具 | **写入** 或 **管理员** |
|
||||
| 查看可用的 MCP 工具 | **读取**、**写入** 或 **管理员** |
|
||||
| 执行 MCP 工具块 | **写入** 或 **管理员** |
|
||||
| 在设置中配置 MCP 服务器 | **Admin** |
|
||||
| 在代理中使用 MCP 工具 | **Write** 或 **Admin** |
|
||||
| 查看可用的 MCP 工具 | **Read**、**Write** 或 **Admin** |
|
||||
| 执行 MCP 工具块 | **Write** 或 **Admin** |
|
||||
|
||||
## 常见使用场景
|
||||
## 常见用例
|
||||
|
||||
### 数据库集成
|
||||
连接到数据库以在工作流中查询、插入或更新数据。
|
||||
在你的工作流中连接数据库以查询、插入或更新数据。
|
||||
|
||||
### API 集成
|
||||
访问没有内置 Sim 集成的外部 API 和 Web 服务。
|
||||
|
||||
### 文件系统访问
|
||||
读取、写入和操作本地或远程文件系统上的文件。
|
||||
在本地或远程文件系统上读取、写入和操作文件。
|
||||
|
||||
### 自定义业务逻辑
|
||||
执行特定于您组织需求的自定义脚本或工具。
|
||||
执行针对你组织需求的自定义脚本或工具。
|
||||
|
||||
### 实时数据访问
|
||||
在工作流执行期间从外部系统获取实时数据。
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- MCP 服务器以配置它的用户权限运行
|
||||
- 安装前始终验证 MCP 服务器来源
|
||||
- 对于敏感的配置数据,请使用环境变量
|
||||
- 在授予代理访问权限之前,审查 MCP 服务器功能
|
||||
- MCP 服务器以配置它们的用户权限运行
|
||||
- 安装前务必验证 MCP 服务器来源
|
||||
- 对敏感配置信息使用环境变量
|
||||
- 在授予代理访问权限前,审查 MCP 服务器的功能
|
||||
|
||||
## 故障排除
|
||||
## 故障排查
|
||||
|
||||
### MCP 服务器未显示
|
||||
- 验证服务器配置是否正确
|
||||
- 检查您是否具有所需权限
|
||||
- 检查你是否拥有所需权限
|
||||
- 确保 MCP 服务器正在运行且可访问
|
||||
|
||||
### 工具执行失败
|
||||
- 验证工具参数格式是否正确
|
||||
- 检查 MCP 服务器日志中的错误消息
|
||||
- 检查 MCP 服务器日志中的错误信息
|
||||
- 确保已配置所需的身份验证
|
||||
|
||||
### 权限错误
|
||||
- 确认您的工作区权限级别
|
||||
- 检查 MCP 服务器是否需要额外的身份验证
|
||||
- 验证服务器是否已为您的工作区正确配置
|
||||
- 确认你的工作区权限级别
|
||||
- 检查 MCP 服务器是否需要额外认证
|
||||
- 验证服务器是否已为你的工作区正确配置
|
||||
486
apps/docs/content/docs/zh/tools/jira_service_management.mdx
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Jira Service Management
|
||||
description: 与 Jira Service Management 互动
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="jira_service_management"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## 使用说明
|
||||
|
||||
集成 Jira Service Management 以进行 IT 服务管理。可创建和管理服务请求,处理客户和组织,跟踪 SLA,并管理队列。
|
||||
|
||||
## 工具
|
||||
|
||||
### `jsm_get_service_desks`
|
||||
|
||||
获取 Jira Service Management 中的所有服务台
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `serviceDesks` | json | 服务台数组 |
|
||||
| `total` | number | 服务台总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_get_request_types`
|
||||
|
||||
获取 Jira Service Management 中某个服务台的请求类型
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要获取请求类型的服务台 ID |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `requestTypes` | json | 请求类型的数组 |
|
||||
| `total` | number | 请求类型的总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_create_request`
|
||||
|
||||
在 Jira Service Management 中创建新的服务请求
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | ------ | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要创建请求的服务台 ID |
|
||||
| `requestTypeId` | string | 是 | 新请求的请求类型 ID |
|
||||
| `summary` | string | 是 | 服务请求的摘要/标题 |
|
||||
| `description` | string | 否 | 服务请求的描述 |
|
||||
| `raiseOnBehalfOf` | string | 否 | 代表客户提交请求的账户 ID |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueId` | string | 创建的请求问题 ID |
|
||||
| `issueKey` | string | 创建的请求问题键(例如 SD-123) |
|
||||
| `requestTypeId` | string | 请求类型 ID |
|
||||
| `serviceDeskId` | string | 服务台 ID |
|
||||
| `success` | boolean | 请求是否创建成功 |
|
||||
| `url` | string | 创建的请求的 URL |
|
||||
|
||||
### `jsm_get_request`
|
||||
|
||||
从 Jira Service Management 获取单个服务请求
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net ) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键(例如 SD-123 ) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
|
||||
### `jsm_get_requests`
|
||||
|
||||
从 Jira Service Management 获取多个服务请求
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net ) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 否 | 按服务台 ID 过滤 |
|
||||
| `requestOwnership` | string | 否 | 按所有权过滤: OWNED_REQUESTS 、 PARTICIPATED_REQUESTS 、 ORGANIZATION 、 ALL_REQUESTS |
|
||||
| `requestStatus` | string | 否 | 按状态过滤: OPEN 、 CLOSED 、 ALL |
|
||||
| `searchTerm` | string | 否 | 用于筛选请求的搜索词 |
|
||||
| `start` | number | 否 | 分页起始索引(默认值: 0 ) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值: 50 ) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `requests` | json | 服务请求数组 |
|
||||
| `total` | number | 请求总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_add_comment`
|
||||
|
||||
在 Jira Service Management 中为服务请求添加评论(公开或内部)
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如 SD-123) |
|
||||
| `body` | string | 是 | 评论正文内容 |
|
||||
| `isPublic` | boolean | 是 | 评论是公开(对客户可见)还是内部 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或关键字 |
|
||||
| `commentId` | string | 创建的评论 ID |
|
||||
| `body` | string | 评论正文内容 |
|
||||
| `isPublic` | boolean | 评论是否为公开 |
|
||||
| `success` | boolean | 评论是否添加成功 |
|
||||
|
||||
### `jsm_get_comments`
|
||||
|
||||
获取 Jira Service Management 中服务请求的评论
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如 SD-123) |
|
||||
| `isPublic` | boolean | 否 | 仅筛选公开评论 |
|
||||
| `internal` | boolean | 否 | 仅筛选内部评论 |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `comments` | json | 评论数组 |
|
||||
| `total` | number | 评论总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_get_customers`
|
||||
|
||||
获取 Jira Service Management 服务台的客户
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要获取客户的服务台 ID |
|
||||
| `query` | string | 否 | 用于筛选客户的搜索查询 |
|
||||
| `start` | number | 否 | 分页的起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `customers` | json | 客户数组 |
|
||||
| `total` | number | 客户总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_add_customer`
|
||||
|
||||
向 Jira Service Management 服务台添加客户
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要添加客户的服务台 ID |
|
||||
| `emails` | string | 是 | 以逗号分隔的要添加为客户的邮箱地址 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `serviceDeskId` | string | 服务台 ID |
|
||||
| `success` | boolean | 是否成功添加了客户 |
|
||||
|
||||
### `jsm_get_organizations`
|
||||
|
||||
获取 Jira Service Management 服务台的组织
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要获取组织的服务台 ID |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `organizations` | json | 组织数组 |
|
||||
| `total` | number | 组织总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_create_organization`
|
||||
|
||||
在 Jira Service Management 中创建一个新组织
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `name` | string | 是 | 要创建的组织名称 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `organizationId` | string | 已创建组织的 ID |
|
||||
| `name` | string | 已创建组织的名称 |
|
||||
| `success` | boolean | 操作是否成功 |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
|
||||
在 Jira Service Management 中将组织添加到服务台
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要添加组织的服务台 ID |
|
||||
| `organizationId` | string | 是 | 要添加到服务台的组织 ID |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `serviceDeskId` | string | 服务台 ID |
|
||||
| `organizationId` | string | 已添加的组织 ID |
|
||||
| `success` | boolean | 操作是否成功 |
|
||||
|
||||
### `jsm_get_queues`
|
||||
|
||||
获取 Jira Service Management 中服务台的队列
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `serviceDeskId` | string | 是 | 要获取队列的服务台 ID |
|
||||
| `includeCount` | boolean | 否 | 是否包含每个队列的问题数量 |
|
||||
| `start` | number | 否 | 分页的起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `queues` | json | 队列数组 |
|
||||
| `total` | number | 队列总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_get_sla`
|
||||
|
||||
获取 Jira Service Management 中服务请求的 SLA 信息
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如:SD-123) |
|
||||
| `start` | number | 否 | 分页的起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `slas` | json | SLA 信息数组 |
|
||||
| `total` | number | SLA 总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_get_transitions`
|
||||
|
||||
获取 Jira Service Management 中服务请求的可用流转
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如:SD-123) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `transitions` | json | 可用流转的数组 |
|
||||
|
||||
### `jsm_transition_request`
|
||||
|
||||
将服务请求流转到 Jira Service Management 中的新状态
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如:SD-123) |
|
||||
| `transitionId` | string | 是 | 要应用的流转 ID |
|
||||
| `comment` | string | 否 | 流转时可选的备注 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `transitionId` | string | 已应用的转换 ID |
|
||||
| `success` | boolean | 转换是否成功 |
|
||||
|
||||
### `jsm_get_participants`
|
||||
|
||||
获取 Jira Service Management 请求的参与者
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `participants` | json | 参与者数组 |
|
||||
| `total` | number | 参与者总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_add_participants`
|
||||
|
||||
为 Jira Service Management 请求添加参与者
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
|
||||
| `accountIds` | string | 是 | 以逗号分隔的要添加为参与者的账户 ID
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `participants` | json | 新增参与者数组 |
|
||||
| `success` | boolean | 操作是否成功 |
|
||||
|
||||
### `jsm_get_approvals`
|
||||
|
||||
在 Jira Service Management 中获取请求的审批信息
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
|
||||
| `start` | number | 否 | 分页起始索引(默认值:0) |
|
||||
| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或键值 |
|
||||
| `approvals` | json | 审批数组 |
|
||||
| `total` | number | 审批总数 |
|
||||
| `isLastPage` | boolean | 是否为最后一页 |
|
||||
|
||||
### `jsm_answer_approval`
|
||||
|
||||
在 Jira Service Management 中批准或拒绝审批请求
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
|
||||
| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
|
||||
| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
|
||||
| `approvalId` | string | 是 | 需要答复的审批 ID |
|
||||
| `decision` | string | 是 | 决策:“approve” 或 “decline” |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueIdOrKey` | string | 问题 ID 或 key |
|
||||
| `approvalId` | string | 审批 ID |
|
||||
| `decision` | string | 做出的决定(approve/decline) |
|
||||
| `success` | boolean | 操作是否成功 |
|
||||
|
||||
## 备注
|
||||
|
||||
- 分类:`tools`
|
||||
- 类型:`jira_service_management`
|
||||
@@ -4333,43 +4333,46 @@ checksums:
|
||||
content/42: 824d6c95976aa25ee803b3591fb6f551
|
||||
content/43: 864966620e19c8746d8180efdbdd9ae2
|
||||
d73ad89134a95ad7e09fa05b910065aa:
|
||||
meta/title: f313501a9bef9974d92f1d62b42fdb93
|
||||
content/0: 0d95e6a3f94bfb96c6dc0d0784c08ea4
|
||||
meta/title: 9c04618798e86e654d5079fe8de64db1
|
||||
meta/description: 4da9477901d9df3c9450ba039d75dd37
|
||||
content/0: 0d1f255ba4a6e466a883628166c021ea
|
||||
content/1: 1f65550d1e29bd4153aff03620240d9a
|
||||
content/2: d5b2535caa6733200d5bfe237593ce3c
|
||||
content/3: b712a1e4dd21fd6b0e930199354adcc3
|
||||
content/4: ca67b9905e9e11c157e61b0ab389538f
|
||||
content/5: 6eee8c607e72b6c444d7b3ef07244f20
|
||||
content/6: 747991e0e80e306dce1061ef7802db2a
|
||||
content/7: 704a7a653c2dc25f6695a6744ca751d3
|
||||
content/7: 430153eacb29c66026cf71944df7be20
|
||||
content/8: 5950966e19939b7a3a320d56ee4a674c
|
||||
content/9: 159cf7a6d62e64b0c5db27e73b8c1ff5
|
||||
content/10: 5bcbd52289426d30d910b18bd3906752
|
||||
content/11: eea4f7ee41a1df57efbc383261657577
|
||||
content/12: 228fb76870c6a4f77ede7ee70b9e88ae
|
||||
content/13: 220fe4f7ae4350eb17a45ca7824f1588
|
||||
content/14: 8c8337d290379b6e1ed445e9be94a9d1
|
||||
content/15: 5d31ac96fab4fe61b0c50b04d3c5334d
|
||||
content/16: b6daf5b4a671686abe262dc86e1a8455
|
||||
content/17: 80c0384b94d64548d753f8029c2bba7f
|
||||
content/18: 606502329e2f62e12f33e39a640ceb10
|
||||
content/19: b57cfd7d01cf1ce7566d9adfd7629e93
|
||||
content/20: 3beb1b867645797faddd37a84bcb6277
|
||||
content/21: 47eb215a0fc230dc651b7bc05ab25ed0
|
||||
content/22: 175a21923c1df473224c54959ecbdb57
|
||||
content/23: 8305e779bb6f866f2e2998c374bc8af0
|
||||
content/24: a6b82eda019a997e4cb55f4461d0ae16
|
||||
content/25: ce8fdb26d2fcbd3f47f1032077622719
|
||||
content/26: 97855f8f10fd385774bc2dde42f96540
|
||||
content/27: 06cd80699da60d9bcce09ee32c0136fc
|
||||
content/28: ebaa0614d49146e17b04fb3be190209f
|
||||
content/29: 14d09c6d97ba08f6e7ea6be3ed854cad
|
||||
content/30: 90fb56f9810d8f8803f19be7907bee90
|
||||
content/31: fbf5d3ade971a3e039c715962db85ea9
|
||||
content/32: 623d40dc1cfdd82c4d805d6b02471c75
|
||||
content/33: 03c45c32e80d7d8875d339a0640f2f63
|
||||
content/34: 3d01b1e7080fee49ffb40b179873b676
|
||||
content/35: 81dcc5343377a51a32fe8d23fc172808
|
||||
content/10: a723187777f9a848d4daa563e9dcbe17
|
||||
content/11: b1c5f14e5290bcbbf5d590361ee7c053
|
||||
content/12: 5bcbd52289426d30d910b18bd3906752
|
||||
content/13: eea4f7ee41a1df57efbc383261657577
|
||||
content/14: 228fb76870c6a4f77ede7ee70b9e88ae
|
||||
content/15: 220fe4f7ae4350eb17a45ca7824f1588
|
||||
content/16: 8c8337d290379b6e1ed445e9be94a9d1
|
||||
content/17: 5d31ac96fab4fe61b0c50b04d3c5334d
|
||||
content/18: b6daf5b4a671686abe262dc86e1a8455
|
||||
content/19: 80c0384b94d64548d753f8029c2bba7f
|
||||
content/20: 606502329e2f62e12f33e39a640ceb10
|
||||
content/21: b57cfd7d01cf1ce7566d9adfd7629e93
|
||||
content/22: 3beb1b867645797faddd37a84bcb6277
|
||||
content/23: 47eb215a0fc230dc651b7bc05ab25ed0
|
||||
content/24: 175a21923c1df473224c54959ecbdb57
|
||||
content/25: 8305e779bb6f866f2e2998c374bc8af0
|
||||
content/26: a6b82eda019a997e4cb55f4461d0ae16
|
||||
content/27: ce8fdb26d2fcbd3f47f1032077622719
|
||||
content/28: 97855f8f10fd385774bc2dde42f96540
|
||||
content/29: 06cd80699da60d9bcce09ee32c0136fc
|
||||
content/30: ebaa0614d49146e17b04fb3be190209f
|
||||
content/31: 14d09c6d97ba08f6e7ea6be3ed854cad
|
||||
content/32: 90fb56f9810d8f8803f19be7907bee90
|
||||
content/33: fbf5d3ade971a3e039c715962db85ea9
|
||||
content/34: 623d40dc1cfdd82c4d805d6b02471c75
|
||||
content/35: 03c45c32e80d7d8875d339a0640f2f63
|
||||
content/36: 3d01b1e7080fee49ffb40b179873b676
|
||||
content/37: 81dcc5343377a51a32fe8d23fc172808
|
||||
936c6450f0e3755fffa26ec3d3bd1b54:
|
||||
meta/title: 2e89ff9f9632dacf671ce83787447240
|
||||
content/0: 7e581dbf3e581d503ac94f7fb7938b1f
|
||||
@@ -50000,3 +50003,166 @@ checksums:
|
||||
content/34: 492b7c5af2dd4be062ee7af19778566a
|
||||
content/35: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/36: 1305f85599a560b30c009091311a8dd0
|
||||
f2beb64780f07155f616b02c172915c6:
|
||||
meta/title: 41848121188bc58a7408e2aff659cd34
|
||||
meta/description: 3d61074bd3acd724d95305b4d7d798bf
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: 83ed540b35d2677d62ac9b033da7d19c
|
||||
content/2: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/3: 418dd7b6606b262ad61dfc2ef4cbbb4c
|
||||
content/4: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/5: f45769b9935e467387f9d6b68603f25e
|
||||
content/6: 8538751b324dd23fcc266ba0188d0130
|
||||
content/7: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/8: 94412648539ad87efa73bcbab074c988
|
||||
content/9: bcadfc362b69078beee0088e5936c98b
|
||||
content/10: a89c7807e429bb51dd48aa4d2622d0dd
|
||||
content/11: b6d0f2ea216b0007e3c3517a3fa50f1f
|
||||
content/12: 45d60212632f9731ddb5cdb827a515ce
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 0d20d22c2c79f1cd78472a3ef7da95bf
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: bccb2c91f666ad27d903f036a75b151e
|
||||
content/17: d2ab825fd4503dbb095177db458d0ff6
|
||||
content/18: f88ad4a7c154ebf76395c29a9955ab94
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: 7cb6471ac338ca8c1423461eb28a057c
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: 8e5505de3c0649f0a9fd2881a040e409
|
||||
content/23: 1d5cf68c4490f3c5cabb2504eecddb5b
|
||||
content/24: 34d6df7a1bf901b2207a52db746a50f2
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 862891a65f07cf068ddbaf046b991f9a
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: 528e47881ef5db3c680d46e80e55f2d6
|
||||
content/29: bc4cb64a528959a7374e1b402f122dfc
|
||||
content/30: 77e2592a86dc0ca50e4db95d808be140
|
||||
content/31: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/32: dea2761ea8687fa90bd2d0fcef2fda0d
|
||||
content/33: bcadfc362b69078beee0088e5936c98b
|
||||
content/34: a617913c5160e5e3ce253e2c7ca82dc5
|
||||
content/35: 33708ed18682a449e15de7f9b576d5f4
|
||||
content/36: a2f0f91ad5d9e03fd823ed21373b379b
|
||||
content/37: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/38: ec6f414795d5257cb7eb66c5018533b2
|
||||
content/39: bcadfc362b69078beee0088e5936c98b
|
||||
content/40: 5e5a3cd4cbc4ee48f0a67c3664258fb2
|
||||
content/41: a78519a6d0969da4eb60984f1c50de03
|
||||
content/42: 1da9ef7f65dba2f4f0d2b1aa9ddb0ccc
|
||||
content/43: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/44: 31c757a9587fae5c97f938d711e7887b
|
||||
content/45: bcadfc362b69078beee0088e5936c98b
|
||||
content/46: d95ba48f24360d807d155a6f8a5bb0be
|
||||
content/47: 3debe47c548eabd98c556240e9d1d573
|
||||
content/48: 0a55d3ddc8c8edfdf1f372f77ad5e231
|
||||
content/49: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/50: 7cf573d553835bd8e716190488602db4
|
||||
content/51: bcadfc362b69078beee0088e5936c98b
|
||||
content/52: bf998d73f67b41c2d9a52bc6a2245179
|
||||
content/53: 47b1f90d885890f4a9f7d2eb1e4a1eb2
|
||||
content/54: 3a0804975c50bb33b204c133ae4c4da2
|
||||
content/55: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/56: 67238743859f40058d7e4328c6bd072f
|
||||
content/57: bcadfc362b69078beee0088e5936c98b
|
||||
content/58: 9e71ffac1d3e6fa23250d1bca33fdd50
|
||||
content/59: ef72212f9c155dcdf3a98bc4a369ee09
|
||||
content/60: 3fce53203dda68c2d1f9dc901a44b747
|
||||
content/61: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/62: 2815e96f98e47e7f7e5b16f68197f084
|
||||
content/63: bcadfc362b69078beee0088e5936c98b
|
||||
content/64: 65890385f788ca17597ce661951fa756
|
||||
content/65: 8228362e54f2a2434467447d7d8075fa
|
||||
content/66: fe2846cd82fcd2515d3c7ad83b50141b
|
||||
content/67: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/68: b7afc8fa3b22ea9327e336f50b82a27c
|
||||
content/69: bcadfc362b69078beee0088e5936c98b
|
||||
content/70: 0337e5d7f0bad113be176419350a41b6
|
||||
content/71: ef61f2bab8cfd25a5228d9df3ff6cf3c
|
||||
content/72: 35a991daf9336e6bba2bd8818dd66594
|
||||
content/73: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/74: 13644af4a2d5aea5061e9945e91f5a4f
|
||||
content/75: bcadfc362b69078beee0088e5936c98b
|
||||
content/76: f3871a9f36a24d642b6de144d605197a
|
||||
content/77: e301551365a6f7fade24239df33463cd
|
||||
content/78: 46100cc58e4f8c1a4c742c1a5e970d0d
|
||||
content/79: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/80: 15e6d6b333eb6a7937028fb886a77e7c
|
||||
content/81: bcadfc362b69078beee0088e5936c98b
|
||||
content/82: 644cb1bcde4e6f1e786b609e74ce43f3
|
||||
content/83: ae715f7048477268f09b33335cf4be93
|
||||
content/84: be625f1454ab49d0eeedb8c2525d8fee
|
||||
content/85: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/86: b4143bf400f52a970c236796fdf9fd03
|
||||
content/87: bcadfc362b69078beee0088e5936c98b
|
||||
content/88: 24af8db55301ef64e8d1bcb53b0a5131
|
||||
content/89: 1d3e6443f80e5a643ff2a4a59544e727
|
||||
content/90: 89dca3d2312aadf8b5cc015e0c84e3eb
|
||||
content/91: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/92: 862891a65f07cf068ddbaf046b991f9a
|
||||
content/93: bcadfc362b69078beee0088e5936c98b
|
||||
content/94: c365464e4cdae303e61cfc38e35887a0
|
||||
content/95: 0afd6c6ee3ecf06afeea0aaa22b19d8e
|
||||
content/96: d78424fb20ea8b940186b2e0ef0fac55
|
||||
content/97: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/98: 4fba9291868e706f90871cdfe0bd2dd7
|
||||
content/99: bcadfc362b69078beee0088e5936c98b
|
||||
content/100: dbd4a81d93749c9c9265b64aff763d93
|
||||
content/101: 1b209b190d6de2af90453ddf6952f480
|
||||
content/102: cec7894f0f14602581914ad3a173ce43
|
||||
content/103: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/104: b4143bf400f52a970c236796fdf9fd03
|
||||
content/105: bcadfc362b69078beee0088e5936c98b
|
||||
content/106: 83e0edf0ff07b297aab822804e185af7
|
||||
content/107: 99e6993e88f7da71bf8e63db3bf2d07f
|
||||
content/108: aefc3699ebb31a0046c6480586e66b5b
|
||||
content/109: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/110: 0d8f09023861380195cba39d5c78ddc5
|
||||
content/111: bcadfc362b69078beee0088e5936c98b
|
||||
content/112: 94455a7b04b702657ae1e68207d70bb9
|
||||
content/113: 95dc72e389fd1b9a8db155882437a5ef
|
||||
content/114: cadf3a887a76a7a268eb8292d26c8cfd
|
||||
content/115: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/116: b4143bf400f52a970c236796fdf9fd03
|
||||
content/117: bcadfc362b69078beee0088e5936c98b
|
||||
content/118: c034d82e3afd5eb98e52b6d46db211f8
|
||||
content/119: a5634cdb8889310cdb3d308a2352e150
|
||||
content/120: 8349f804b5bc2a5580197e4fd848270e
|
||||
content/121: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/122: d9576628d658089f122e5fafb200c34c
|
||||
content/123: bcadfc362b69078beee0088e5936c98b
|
||||
content/124: 7be2eecb48d34398e118f223e7947adc
|
||||
content/125: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/126: b6825d890bccce15d748ceb727d30104
|
||||
f3ceca041234b3c5122b03bc11e4d1c1:
|
||||
meta/title: 960685e215a11e2b38285dff5b0dde47
|
||||
meta/description: 671e4d9e7ed6dd8b7774dcd4cfbecade
|
||||
content/0: 833213d7266febc5d9ac218725cfb057
|
||||
content/1: 10dedb2b36131c07ec48f97ece922c8c
|
||||
content/2: b082096b0c871b2a40418e479af6f158
|
||||
content/3: 9c94aa34f44540b0632931a8244a6488
|
||||
content/4: 14f33e16b5a98e4dbdda2a27aa0d7afb
|
||||
content/5: d7b36732970b7649dd1aa1f1d0a34e74
|
||||
content/6: f554f833467a6dae5391372fc41dad53
|
||||
content/7: 9cdb9189ecfcc4a6f567d3fd5fe342f0
|
||||
content/8: 9a107692cb52c284c1cb022b516d700b
|
||||
content/9: 07a013a9b263ab0ae4458db97065bdcd
|
||||
content/10: 9310a48f3e485c5709563f1b825eb32d
|
||||
content/11: 8a2c3d1a1a30e3614ada44b88478cc0c
|
||||
content/12: defcb9a4ec64b567f45c3669c214763f
|
||||
content/13: 4f3202eff0734a7398445d8c54f9e3ad
|
||||
content/14: afcee4eacb27fb678e159c512d114c2d
|
||||
content/15: 4ecff63a3571ef6f519a2448931451c2
|
||||
content/16: 880b1c60228a0b56c5eb62dac87792df
|
||||
content/17: d3f79ae3be3fe3ca4df5bd59be6b404c
|
||||
content/18: 028eb92d4776faeb029995cee976bfc4
|
||||
content/19: a618fcff50c4856113428639359a922b
|
||||
content/20: 5fd3a6d2dcd8aa18dbf0b784acaa271c
|
||||
content/21: d118656dd565c4c22f3c0c3a7c7f3bee
|
||||
content/22: f49b9be78f1e7a569e290acc1365d417
|
||||
content/23: 0a70ebe6eb4c543c3810977ed46b69b0
|
||||
content/24: ad8638a3473c909dbcb1e1d9f4f26381
|
||||
content/25: 95343a9f81cd050d3713988c677c750f
|
||||
content/26: d4f846a591ac7fdedaba281b44d50ae3
|
||||
content/27: 764eb0e5d025b68f772d45adb7608349
|
||||
content/28: 47eb215a0fc230dc651b7bc05ab25ed0
|
||||
content/29: bf5c6bf1e75c5c5e3a0a5dd1314cb41e
|
||||
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 155 KiB |
@@ -3,6 +3,7 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { X } from 'lucide-react'
|
||||
import { Textarea } from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
import { FAQ } from '@/lib/blog/faq'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
|
||||
interface Author {
|
||||
id: string
|
||||
|
||||
@@ -21,12 +21,13 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
pathname.startsWith('/careers') ||
|
||||
pathname.startsWith('/changelog') ||
|
||||
pathname.startsWith('/chat') ||
|
||||
pathname.startsWith('/studio')
|
||||
pathname.startsWith('/studio') ||
|
||||
pathname.startsWith('/resume')
|
||||
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute='class'
|
||||
defaultTheme='dark'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
storageKey='sim-theme'
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
/**
|
||||
* CSS-based sidebar and panel widths to prevent SSR hydration mismatches.
|
||||
* Default widths are set here and updated via blocking script before React hydrates.
|
||||
*
|
||||
* @important These values must stay in sync with stores/constants.ts
|
||||
* @see stores/constants.ts for the source of truth
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 232px;
|
||||
--panel-width: 260px;
|
||||
--toolbar-triggers-height: 300px;
|
||||
--editor-connections-height: 200px;
|
||||
--terminal-height: 196px;
|
||||
--sidebar-width: 232px; /* SIDEBAR_WIDTH.DEFAULT */
|
||||
--panel-width: 290px; /* PANEL_WIDTH.DEFAULT */
|
||||
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
||||
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
||||
--terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
|
||||
88
apps/sim/app/api/mcp/discover/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflowMcpServer, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
const logger = createLogger('McpDiscoverAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Discover all MCP servers available to the authenticated user.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Authentication required. Provide X-API-Key header.' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const userId = auth.userId
|
||||
|
||||
const userWorkspacePermissions = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIds = userWorkspacePermissions.map((w) => w.entityId)
|
||||
|
||||
if (workspaceIds.length === 0) {
|
||||
return NextResponse.json({ success: true, servers: [] })
|
||||
}
|
||||
|
||||
const servers = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
workspaceName: workspace.name,
|
||||
createdAt: workflowMcpServer.createdAt,
|
||||
toolCount: sql<number>`(
|
||||
SELECT COUNT(*)::int
|
||||
FROM "workflow_mcp_tool"
|
||||
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
|
||||
)`.as('tool_count'),
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.leftJoin(workspace, eq(workflowMcpServer.workspaceId, workspace.id))
|
||||
.where(sql`${workflowMcpServer.workspaceId} IN ${workspaceIds}`)
|
||||
.orderBy(workflowMcpServer.name)
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const formattedServers = servers.map((server) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
workspace: { id: server.workspaceId, name: server.workspaceName },
|
||||
toolCount: server.toolCount || 0,
|
||||
createdAt: server.createdAt,
|
||||
url: `${baseUrl}/api/mcp/serve/${server.id}`,
|
||||
}))
|
||||
|
||||
logger.info(`User ${userId} discovered ${formattedServers.length} MCP servers`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
servers: formattedServers,
|
||||
authentication: {
|
||||
method: 'API Key',
|
||||
header: 'X-API-Key',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error discovering MCP servers:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to discover MCP servers' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
306
apps/sim/app/api/mcp/serve/[serverId]/route.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* MCP Serve Endpoint - Implements MCP protocol for workflow servers using SDK types.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CallToolResult,
|
||||
ErrorCode,
|
||||
type InitializeResult,
|
||||
isJSONRPCNotification,
|
||||
isJSONRPCRequest,
|
||||
type JSONRPCError,
|
||||
type JSONRPCMessage,
|
||||
type JSONRPCResponse,
|
||||
type ListToolsResult,
|
||||
type RequestId,
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServeAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
serverId: string
|
||||
}
|
||||
|
||||
function createResponse(id: RequestId, result: unknown): JSONRPCResponse {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: result as JSONRPCResponse['result'],
|
||||
}
|
||||
}
|
||||
|
||||
function createError(id: RequestId, code: ErrorCode | number, message: string): JSONRPCError {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code, message },
|
||||
}
|
||||
}
|
||||
|
||||
async function getServer(serverId: string) {
|
||||
const [server] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
const server = await getServer(serverId)
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
name: server.name,
|
||||
version: '1.0.0',
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: { tools: {} },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting MCP server info:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
const server = await getServer(serverId)
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const message = body as JSONRPCMessage
|
||||
|
||||
if (isJSONRPCNotification(message)) {
|
||||
logger.info(`Received notification: ${message.method}`)
|
||||
return new NextResponse(null, { status: 202 })
|
||||
}
|
||||
|
||||
if (!isJSONRPCRequest(message)) {
|
||||
return NextResponse.json(
|
||||
createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'),
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const { id, method, params: rpcParams } = message
|
||||
const apiKey =
|
||||
request.headers.get('X-API-Key') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
switch (method) {
|
||||
case 'initialize': {
|
||||
const result: InitializeResult = {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: server.name, version: '1.0.0' },
|
||||
}
|
||||
return NextResponse.json(createResponse(id, result))
|
||||
}
|
||||
|
||||
case 'ping':
|
||||
return NextResponse.json(createResponse(id, {}))
|
||||
|
||||
case 'tools/list':
|
||||
return handleToolsList(id, serverId)
|
||||
|
||||
case 'tools/call':
|
||||
return handleToolsCall(
|
||||
id,
|
||||
serverId,
|
||||
rpcParams as { name: string; arguments?: Record<string, unknown> },
|
||||
apiKey
|
||||
)
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
createError(id, ErrorCode.MethodNotFound, `Method not found: ${method}`),
|
||||
{
|
||||
status: 404,
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP request:', error)
|
||||
return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), {
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToolsList(id: RequestId, serverId: string): Promise<NextResponse> {
|
||||
try {
|
||||
const tools = await db
|
||||
.select({
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
parameterSchema: workflowMcpTool.parameterSchema,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
const result: ListToolsResult = {
|
||||
tools: tools.map((tool) => {
|
||||
const schema = tool.parameterSchema as {
|
||||
type?: string
|
||||
properties?: Record<string, unknown>
|
||||
required?: string[]
|
||||
} | null
|
||||
return {
|
||||
name: tool.toolName,
|
||||
description: tool.toolDescription || `Execute workflow: ${tool.toolName}`,
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: schema?.properties || {},
|
||||
...(schema?.required && schema.required.length > 0 && { required: schema.required }),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
return NextResponse.json(createResponse(id, result))
|
||||
} catch (error) {
|
||||
logger.error('Error listing tools:', error)
|
||||
return NextResponse.json(createError(id, ErrorCode.InternalError, 'Failed to list tools'), {
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToolsCall(
|
||||
id: RequestId,
|
||||
serverId: string,
|
||||
params: { name: string; arguments?: Record<string, unknown> } | undefined,
|
||||
apiKey?: string | null
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
if (!params?.name) {
|
||||
return NextResponse.json(createError(id, ErrorCode.InvalidParams, 'Tool name required'), {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
const [tool] = await db
|
||||
.select({
|
||||
toolName: workflowMcpTool.toolName,
|
||||
workflowId: workflowMcpTool.workflowId,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.toolName, params.name)))
|
||||
.limit(1)
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
createError(id, ErrorCode.InvalidParams, `Tool not found: ${params.name}`),
|
||||
{
|
||||
status: 404,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const [wf] = await db
|
||||
.select({ isDeployed: workflow.isDeployed })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, tool.workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!wf?.isDeployed) {
|
||||
return NextResponse.json(
|
||||
createError(id, ErrorCode.InternalError, 'Workflow is not deployed'),
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (apiKey) headers['X-API-Key'] = apiKey
|
||||
|
||||
logger.info(`Executing workflow ${tool.workflowId} via MCP tool ${params.name}`)
|
||||
|
||||
const response = await fetch(executeUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
||||
signal: AbortSignal.timeout(300000), // 5 minute timeout
|
||||
})
|
||||
|
||||
const executeResult = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
createError(
|
||||
id,
|
||||
ErrorCode.InternalError,
|
||||
executeResult.error || 'Workflow execution failed'
|
||||
),
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const result: CallToolResult = {
|
||||
content: [
|
||||
{ type: 'text', text: JSON.stringify(executeResult.output || executeResult, null, 2) },
|
||||
],
|
||||
isError: !executeResult.success,
|
||||
}
|
||||
|
||||
return NextResponse.json(createResponse(id, result))
|
||||
} catch (error) {
|
||||
logger.error('Error calling tool:', error)
|
||||
return NextResponse.json(createError(id, ErrorCode.InternalError, 'Tool execution failed'), {
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
const server = await getServer(serverId)
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.info(`MCP session terminated for server ${serverId}`)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP DELETE request:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,150 @@
|
||||
import { db } from '@sim/db'
|
||||
import { mcpServers } from '@sim/db/schema'
|
||||
import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import type { McpServerStatusConfig } from '@/lib/mcp/types'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import type { McpServerStatusConfig, McpTool, McpToolSchema } from '@/lib/mcp/types'
|
||||
import {
|
||||
createMcpErrorResponse,
|
||||
createMcpSuccessResponse,
|
||||
MCP_TOOL_CORE_PARAMS,
|
||||
} from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('McpServerRefreshAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Schema stored in workflow blocks includes description from the tool. */
|
||||
type StoredToolSchema = McpToolSchema & { description?: string }
|
||||
|
||||
interface StoredTool {
|
||||
type: string
|
||||
title: string
|
||||
toolId: string
|
||||
params: {
|
||||
serverId: string
|
||||
serverUrl?: string
|
||||
toolName: string
|
||||
serverName?: string
|
||||
}
|
||||
schema?: StoredToolSchema
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
updatedCount: number
|
||||
updatedWorkflowIds: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Refresh an MCP server connection (requires any workspace permission)
|
||||
* Syncs tool schemas from discovered MCP tools to all workflow blocks using those tools.
|
||||
* Returns the count and IDs of updated workflows.
|
||||
*/
|
||||
async function syncToolSchemasToWorkflows(
|
||||
workspaceId: string,
|
||||
serverId: string,
|
||||
tools: McpTool[],
|
||||
requestId: string
|
||||
): Promise<SyncResult> {
|
||||
const toolsByName = new Map(tools.map((t) => [t.name, t]))
|
||||
|
||||
const workspaceWorkflows = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.workspaceId, workspaceId))
|
||||
|
||||
const workflowIds = workspaceWorkflows.map((w) => w.id)
|
||||
if (workflowIds.length === 0) return { updatedCount: 0, updatedWorkflowIds: [] }
|
||||
|
||||
const agentBlocks = await db
|
||||
.select({
|
||||
id: workflowBlocks.id,
|
||||
workflowId: workflowBlocks.workflowId,
|
||||
subBlocks: workflowBlocks.subBlocks,
|
||||
})
|
||||
.from(workflowBlocks)
|
||||
.where(eq(workflowBlocks.type, 'agent'))
|
||||
|
||||
const updatedWorkflowIds = new Set<string>()
|
||||
|
||||
for (const block of agentBlocks) {
|
||||
if (!workflowIds.includes(block.workflowId)) continue
|
||||
|
||||
const subBlocks = block.subBlocks as Record<string, unknown> | null
|
||||
if (!subBlocks) continue
|
||||
|
||||
const toolsSubBlock = subBlocks.tools as { value?: StoredTool[] } | undefined
|
||||
if (!toolsSubBlock?.value || !Array.isArray(toolsSubBlock.value)) continue
|
||||
|
||||
let hasUpdates = false
|
||||
const updatedTools = toolsSubBlock.value.map((tool) => {
|
||||
if (tool.type !== 'mcp' || tool.params?.serverId !== serverId) {
|
||||
return tool
|
||||
}
|
||||
|
||||
const freshTool = toolsByName.get(tool.params.toolName)
|
||||
if (!freshTool) return tool
|
||||
|
||||
const newSchema: StoredToolSchema = {
|
||||
...freshTool.inputSchema,
|
||||
description: freshTool.description,
|
||||
}
|
||||
|
||||
const schemasMatch = JSON.stringify(tool.schema) === JSON.stringify(newSchema)
|
||||
|
||||
if (!schemasMatch) {
|
||||
hasUpdates = true
|
||||
|
||||
const validParamKeys = new Set(Object.keys(newSchema.properties || {}))
|
||||
|
||||
const cleanedParams: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(tool.params || {})) {
|
||||
if (MCP_TOOL_CORE_PARAMS.has(key) || validParamKeys.has(key)) {
|
||||
cleanedParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return { ...tool, schema: newSchema, params: cleanedParams }
|
||||
}
|
||||
|
||||
return tool
|
||||
})
|
||||
|
||||
if (hasUpdates) {
|
||||
const updatedSubBlocks = {
|
||||
...subBlocks,
|
||||
tools: { ...toolsSubBlock, value: updatedTools },
|
||||
}
|
||||
|
||||
await db
|
||||
.update(workflowBlocks)
|
||||
.set({ subBlocks: updatedSubBlocks, updatedAt: new Date() })
|
||||
.where(eq(workflowBlocks.id, block.id))
|
||||
|
||||
updatedWorkflowIds.add(block.workflowId)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedWorkflowIds.size > 0) {
|
||||
logger.info(
|
||||
`[${requestId}] Synced tool schemas to ${updatedWorkflowIds.size} workflow(s) for server ${serverId}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
updatedCount: updatedWorkflowIds.size,
|
||||
updatedWorkflowIds: Array.from(updatedWorkflowIds),
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
const { id: serverId } = await params
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Refreshing MCP server: ${serverId} in workspace: ${workspaceId}`,
|
||||
{
|
||||
userId,
|
||||
}
|
||||
)
|
||||
logger.info(`[${requestId}] Refreshing MCP server: ${serverId}`)
|
||||
|
||||
const [server] = await db
|
||||
.select()
|
||||
@@ -50,6 +169,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
let connectionStatus: 'connected' | 'disconnected' | 'error' = 'error'
|
||||
let toolCount = 0
|
||||
let lastError: string | null = null
|
||||
let syncResult: SyncResult = { updatedCount: 0, updatedWorkflowIds: [] }
|
||||
let discoveredTools: McpTool[] = []
|
||||
|
||||
const currentStatusConfig: McpServerStatusConfig =
|
||||
(server.statusConfig as McpServerStatusConfig | null) ?? {
|
||||
@@ -58,11 +179,16 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
}
|
||||
|
||||
try {
|
||||
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
|
||||
discoveredTools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
|
||||
connectionStatus = 'connected'
|
||||
toolCount = tools.length
|
||||
logger.info(
|
||||
`[${requestId}] Successfully connected to server ${serverId}, discovered ${toolCount} tools`
|
||||
toolCount = discoveredTools.length
|
||||
logger.info(`[${requestId}] Discovered ${toolCount} tools from server ${serverId}`)
|
||||
|
||||
syncResult = await syncToolSchemasToWorkflows(
|
||||
workspaceId,
|
||||
serverId,
|
||||
discoveredTools,
|
||||
requestId
|
||||
)
|
||||
} catch (error) {
|
||||
connectionStatus = 'error'
|
||||
@@ -94,14 +220,7 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
.returning()
|
||||
|
||||
if (connectionStatus === 'connected') {
|
||||
logger.info(
|
||||
`[${requestId}] Successfully refreshed MCP server: ${serverId} (${toolCount} tools)`
|
||||
)
|
||||
await mcpService.clearCache(workspaceId)
|
||||
} else {
|
||||
logger.warn(
|
||||
`[${requestId}] Refresh completed for MCP server ${serverId} but connection failed: ${lastError}`
|
||||
)
|
||||
}
|
||||
|
||||
return createMcpSuccessResponse({
|
||||
@@ -109,6 +228,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
toolCount,
|
||||
lastConnected: refreshedServer?.lastConnected?.toISOString() || null,
|
||||
error: lastError,
|
||||
workflowsUpdated: syncResult.updatedCount,
|
||||
updatedWorkflowIds: syncResult.updatedWorkflowIds,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error refreshing MCP server:`, error)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { and, eq, isNull } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('McpServerAPI')
|
||||
@@ -27,24 +26,6 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
updates: Object.keys(body).filter((k) => k !== 'workspaceId'),
|
||||
})
|
||||
|
||||
// Validate URL if being updated
|
||||
if (
|
||||
body.url &&
|
||||
(body.transport === 'http' ||
|
||||
body.transport === 'sse' ||
|
||||
body.transport === 'streamable-http')
|
||||
) {
|
||||
const urlValidation = validateMcpServerUrl(body.url)
|
||||
if (!urlValidation.isValid) {
|
||||
return createMcpErrorResponse(
|
||||
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
|
||||
'Invalid server URL',
|
||||
400
|
||||
)
|
||||
}
|
||||
body.url = urlValidation.normalizedUrl
|
||||
}
|
||||
|
||||
// Remove workspaceId from body to prevent it from being updated
|
||||
const { workspaceId: _, ...updateData } = body
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import { and, eq, isNull } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import type { McpTransport } from '@/lib/mcp/types'
|
||||
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
|
||||
import {
|
||||
createMcpErrorResponse,
|
||||
createMcpSuccessResponse,
|
||||
@@ -17,13 +15,6 @@ const logger = createLogger('McpServersAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Check if transport type requires a URL
|
||||
*/
|
||||
function isUrlBasedTransport(transport: McpTransport): boolean {
|
||||
return transport === 'streamable-http'
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - List all registered MCP servers for the workspace
|
||||
*/
|
||||
@@ -81,18 +72,6 @@ export const POST = withMcpAuth('write')(
|
||||
)
|
||||
}
|
||||
|
||||
if (isUrlBasedTransport(body.transport) && body.url) {
|
||||
const urlValidation = validateMcpServerUrl(body.url)
|
||||
if (!urlValidation.isValid) {
|
||||
return createMcpErrorResponse(
|
||||
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
|
||||
'Invalid server URL',
|
||||
400
|
||||
)
|
||||
}
|
||||
body.url = urlValidation.normalizedUrl
|
||||
}
|
||||
|
||||
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
|
||||
|
||||
const [existingServer] = await db
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { McpClient } from '@/lib/mcp/client'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
|
||||
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
@@ -89,24 +88,12 @@ export const POST = withMcpAuth('write')(
|
||||
)
|
||||
}
|
||||
|
||||
if (isUrlBasedTransport(body.transport)) {
|
||||
if (!body.url) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('URL is required for HTTP-based transports'),
|
||||
'Missing required URL',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
const urlValidation = validateMcpServerUrl(body.url)
|
||||
if (!urlValidation.isValid) {
|
||||
return createMcpErrorResponse(
|
||||
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
|
||||
'Invalid server URL',
|
||||
400
|
||||
)
|
||||
}
|
||||
body.url = urlValidation.normalizedUrl
|
||||
if (isUrlBasedTransport(body.transport) && !body.url) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('URL is required for HTTP-based transports'),
|
||||
'Missing required URL',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
let resolvedUrl = body.url
|
||||
|
||||
@@ -9,9 +9,6 @@ const logger = createLogger('McpToolDiscoveryAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET - Discover all tools from user's MCP servers
|
||||
*/
|
||||
export const GET = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
@@ -19,18 +16,11 @@ export const GET = withMcpAuth('read')(
|
||||
const serverId = searchParams.get('serverId')
|
||||
const forceRefresh = searchParams.get('refresh') === 'true'
|
||||
|
||||
logger.info(`[${requestId}] Discovering MCP tools for user ${userId}`, {
|
||||
serverId,
|
||||
workspaceId,
|
||||
forceRefresh,
|
||||
})
|
||||
logger.info(`[${requestId}] Discovering MCP tools`, { serverId, workspaceId, forceRefresh })
|
||||
|
||||
let tools
|
||||
if (serverId) {
|
||||
tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
|
||||
} else {
|
||||
tools = await mcpService.discoverTools(userId, workspaceId, forceRefresh)
|
||||
}
|
||||
const tools = serverId
|
||||
? await mcpService.discoverServerTools(userId, serverId, workspaceId)
|
||||
: await mcpService.discoverTools(userId, workspaceId, forceRefresh)
|
||||
|
||||
const byServer: Record<string, number> = {}
|
||||
for (const tool of tools) {
|
||||
@@ -55,9 +45,6 @@ export const GET = withMcpAuth('read')(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST - Refresh tool discovery for specific servers
|
||||
*/
|
||||
export const POST = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
@@ -72,10 +59,7 @@ export const POST = withMcpAuth('read')(
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Refreshing tool discovery for user ${userId}, servers:`,
|
||||
serverIds
|
||||
)
|
||||
logger.info(`[${requestId}] Refreshing tools for ${serverIds.length} servers`)
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
serverIds.map(async (serverId: string) => {
|
||||
@@ -99,7 +83,8 @@ export const POST = withMcpAuth('read')(
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = {
|
||||
logger.info(`[${requestId}] Refresh completed: ${successes.length}/${serverIds.length}`)
|
||||
return createMcpSuccessResponse({
|
||||
refreshed: successes,
|
||||
failed: failures,
|
||||
summary: {
|
||||
@@ -107,12 +92,7 @@ export const POST = withMcpAuth('read')(
|
||||
successful: successes.length,
|
||||
failed: failures.length,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Tool discovery refresh completed: ${successes.length}/${serverIds.length} successful`
|
||||
)
|
||||
return createMcpSuccessResponse(responseData)
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error refreshing tool discovery:`, error)
|
||||
const { message, status } = categorizeError(error)
|
||||
|
||||
@@ -4,39 +4,20 @@ import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import type { McpToolSchema, StoredMcpTool } from '@/lib/mcp/types'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('McpStoredToolsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface StoredMcpTool {
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
serverId: string
|
||||
serverUrl?: string
|
||||
toolName: string
|
||||
schema?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get all stored MCP tools from workflows in the workspace
|
||||
*
|
||||
* Scans all workflows in the workspace and extracts MCP tools that have been
|
||||
* added to agent blocks. Returns the stored state of each tool for comparison
|
||||
* against current server state.
|
||||
*/
|
||||
export const GET = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
logger.info(`[${requestId}] Fetching stored MCP tools for workspace ${workspaceId}`)
|
||||
|
||||
// Get all workflows in workspace
|
||||
const workflows = await db
|
||||
.select({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
})
|
||||
.select({ id: workflow.id, name: workflow.name })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.workspaceId, workspaceId))
|
||||
|
||||
@@ -47,12 +28,8 @@ export const GET = withMcpAuth('read')(
|
||||
return createMcpSuccessResponse({ tools: [] })
|
||||
}
|
||||
|
||||
// Get all agent blocks from these workflows
|
||||
const agentBlocks = await db
|
||||
.select({
|
||||
workflowId: workflowBlocks.workflowId,
|
||||
subBlocks: workflowBlocks.subBlocks,
|
||||
})
|
||||
.select({ workflowId: workflowBlocks.workflowId, subBlocks: workflowBlocks.subBlocks })
|
||||
.from(workflowBlocks)
|
||||
.where(eq(workflowBlocks.type, 'agent'))
|
||||
|
||||
@@ -81,7 +58,7 @@ export const GET = withMcpAuth('read')(
|
||||
serverId: params.serverId as string,
|
||||
serverUrl: params.serverUrl as string | undefined,
|
||||
toolName: params.toolName as string,
|
||||
schema: tool.schema as Record<string, unknown> | undefined,
|
||||
schema: tool.schema as McpToolSchema | undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
155
apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServerAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get a specific workflow MCP server with its tools
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Getting workflow MCP server: ${serverId}`)
|
||||
|
||||
const [server] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
createdBy: workflowMcpServer.createdBy,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
createdAt: workflowMcpServer.createdAt,
|
||||
updatedAt: workflowMcpServer.updatedAt,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const tools = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Found workflow MCP server: ${server.name} with ${tools.length} tools`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ server, tools })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error getting workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to get workflow MCP server'),
|
||||
'Failed to get workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PATCH - Update a workflow MCP server
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Updating workflow MCP server: ${serverId}`)
|
||||
|
||||
const [existingServer] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (body.name !== undefined) {
|
||||
updateData.name = body.name.trim()
|
||||
}
|
||||
if (body.description !== undefined) {
|
||||
updateData.description = body.description?.trim() || null
|
||||
}
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(workflowMcpServer)
|
||||
.set(updateData)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.returning()
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ server: updatedServer })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to update workflow MCP server'),
|
||||
'Failed to update workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE - Delete a workflow MCP server and all its tools
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Deleting workflow MCP server: ${serverId}`)
|
||||
|
||||
const [deletedServer] = await db
|
||||
.delete(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.returning()
|
||||
|
||||
if (!deletedServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to delete workflow MCP server'),
|
||||
'Failed to delete workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
|
||||
|
||||
const logger = createLogger('WorkflowMcpToolAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
toolId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get a specific tool
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Getting tool ${toolId} from server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [tool] = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.limit(1)
|
||||
|
||||
if (!tool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
return createMcpSuccessResponse({ tool })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error getting tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to get tool'),
|
||||
'Failed to get tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PATCH - Update a tool's configuration
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Updating tool ${toolId} in server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [existingTool] = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.limit(1)
|
||||
|
||||
if (!existingTool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (body.toolName !== undefined) {
|
||||
updateData.toolName = sanitizeToolName(body.toolName)
|
||||
}
|
||||
if (body.toolDescription !== undefined) {
|
||||
updateData.toolDescription = body.toolDescription?.trim() || null
|
||||
}
|
||||
if (body.parameterSchema !== undefined) {
|
||||
updateData.parameterSchema = body.parameterSchema
|
||||
}
|
||||
|
||||
const [updatedTool] = await db
|
||||
.update(workflowMcpTool)
|
||||
.set(updateData)
|
||||
.where(eq(workflowMcpTool.id, toolId))
|
||||
.returning()
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated tool ${toolId}`)
|
||||
|
||||
return createMcpSuccessResponse({ tool: updatedTool })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to update tool'),
|
||||
'Failed to update tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE - Remove a tool from an MCP server
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Deleting tool ${toolId} from server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [deletedTool] = await db
|
||||
.delete(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.returning()
|
||||
|
||||
if (!deletedTool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted tool ${toolId}`)
|
||||
|
||||
return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to delete tool'),
|
||||
'Failed to delete tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
223
apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpToolsAPI')
|
||||
|
||||
/**
|
||||
* Check if a workflow has a valid start block by loading from database
|
||||
*/
|
||||
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
|
||||
try {
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
return hasValidStartBlockInState(normalizedData)
|
||||
} catch (error) {
|
||||
logger.warn('Error checking for start block:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - List all tools for a workflow MCP server
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Listing tools for workflow MCP server: ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
// Get tools with workflow details
|
||||
const tools = await db
|
||||
.select({
|
||||
id: workflowMcpTool.id,
|
||||
serverId: workflowMcpTool.serverId,
|
||||
workflowId: workflowMcpTool.workflowId,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
parameterSchema: workflowMcpTool.parameterSchema,
|
||||
createdAt: workflowMcpTool.createdAt,
|
||||
updatedAt: workflowMcpTool.updatedAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
isDeployed: workflow.isDeployed,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.leftJoin(workflow, eq(workflowMcpTool.workflowId, workflow.id))
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
logger.info(`[${requestId}] Found ${tools.length} tools for server ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ tools })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error listing tools:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to list tools'),
|
||||
'Failed to list tools',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST - Add a workflow as a tool to an MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Adding tool to workflow MCP server: ${serverId}`, {
|
||||
workflowId: body.workflowId,
|
||||
})
|
||||
|
||||
if (!body.workflowId) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Missing required field: workflowId'),
|
||||
'Missing required field',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
// Verify workflow exists and is deployed
|
||||
const [workflowRecord] = await db
|
||||
.select({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
isDeployed: workflow.isDeployed,
|
||||
workspaceId: workflow.workspaceId,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, body.workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return createMcpErrorResponse(new Error('Workflow not found'), 'Workflow not found', 404)
|
||||
}
|
||||
|
||||
// Verify workflow belongs to the same workspace
|
||||
if (workflowRecord.workspaceId !== workspaceId) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow does not belong to this workspace'),
|
||||
'Access denied',
|
||||
403
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow must be deployed before adding as a tool'),
|
||||
'Workflow not deployed',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Verify workflow has a valid start block
|
||||
const hasStartBlock = await hasValidStartBlock(body.workflowId)
|
||||
if (!hasStartBlock) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow must have a Start block to be used as an MCP tool'),
|
||||
'No start block found',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Check if tool already exists for this workflow
|
||||
const [existingTool] = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpTool.serverId, serverId),
|
||||
eq(workflowMcpTool.workflowId, body.workflowId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingTool) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('This workflow is already added as a tool to this server'),
|
||||
'Tool already exists',
|
||||
409
|
||||
)
|
||||
}
|
||||
|
||||
const toolName = sanitizeToolName(body.toolName?.trim() || workflowRecord.name)
|
||||
const toolDescription =
|
||||
body.toolDescription?.trim() ||
|
||||
workflowRecord.description ||
|
||||
`Execute ${workflowRecord.name} workflow`
|
||||
|
||||
// Create the tool
|
||||
const toolId = crypto.randomUUID()
|
||||
const [tool] = await db
|
||||
.insert(workflowMcpTool)
|
||||
.values({
|
||||
id: toolId,
|
||||
serverId,
|
||||
workflowId: body.workflowId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterSchema: body.parameterSchema || {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully added tool ${toolName} (workflow: ${body.workflowId}) to server ${serverId}`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ tool }, 201)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error adding tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to add tool'),
|
||||
'Failed to add tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
132
apps/sim/app/api/mcp/workflow-servers/route.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq, inArray, sql } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServersAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET - List all workflow MCP servers for the workspace
|
||||
*/
|
||||
export const GET = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
logger.info(`[${requestId}] Listing workflow MCP servers for workspace ${workspaceId}`)
|
||||
|
||||
const servers = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
createdBy: workflowMcpServer.createdBy,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
createdAt: workflowMcpServer.createdAt,
|
||||
updatedAt: workflowMcpServer.updatedAt,
|
||||
toolCount: sql<number>`(
|
||||
SELECT COUNT(*)::int
|
||||
FROM "workflow_mcp_tool"
|
||||
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
|
||||
)`.as('tool_count'),
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
|
||||
// Fetch all tools for these servers
|
||||
const serverIds = servers.map((s) => s.id)
|
||||
const tools =
|
||||
serverIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
serverId: workflowMcpTool.serverId,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(inArray(workflowMcpTool.serverId, serverIds))
|
||||
: []
|
||||
|
||||
// Group tool names by server
|
||||
const toolNamesByServer: Record<string, string[]> = {}
|
||||
for (const tool of tools) {
|
||||
if (!toolNamesByServer[tool.serverId]) {
|
||||
toolNamesByServer[tool.serverId] = []
|
||||
}
|
||||
toolNamesByServer[tool.serverId].push(tool.toolName)
|
||||
}
|
||||
|
||||
// Attach tool names to servers
|
||||
const serversWithToolNames = servers.map((server) => ({
|
||||
...server,
|
||||
toolNames: toolNamesByServer[server.id] || [],
|
||||
}))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Listed ${servers.length} workflow MCP servers for workspace ${workspaceId}`
|
||||
)
|
||||
return createMcpSuccessResponse({ servers: serversWithToolNames })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error listing workflow MCP servers:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to list workflow MCP servers'),
|
||||
'Failed to list workflow MCP servers',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST - Create a new workflow MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Creating workflow MCP server:`, {
|
||||
name: body.name,
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
if (!body.name) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Missing required field: name'),
|
||||
'Missing required field',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
const serverId = crypto.randomUUID()
|
||||
|
||||
const [server] = await db
|
||||
.insert(workflowMcpServer)
|
||||
.values({
|
||||
id: serverId,
|
||||
workspaceId,
|
||||
createdBy: userId,
|
||||
name: body.name.trim(),
|
||||
description: body.description?.trim() || null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ server }, 201)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to create workflow MCP server'),
|
||||
'Failed to create workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -70,7 +70,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
.update(templates)
|
||||
.set({
|
||||
views: sql`${templates.views} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
.update(templates)
|
||||
.set({
|
||||
stars: sql`${templates.stars} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
})
|
||||
@@ -160,7 +159,6 @@ export async function DELETE(
|
||||
.update(templates)
|
||||
.set({
|
||||
stars: sql`GREATEST(${templates.stars} - 1, 0)`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
})
|
||||
|
||||
@@ -136,7 +136,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
// Prepare template update data
|
||||
const updateData: any = {
|
||||
views: sql`${templates.views} + 1`,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
// If connecting to template for editing, also update the workflowId
|
||||
|
||||
183
apps/sim/app/api/tools/jsm/approvals/route.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmApprovalsAPI')
|
||||
|
||||
const VALID_ACTIONS = ['get', 'answer'] as const
|
||||
const VALID_DECISIONS = ['approve', 'decline'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
issueIdOrKey,
|
||||
approvalId,
|
||||
decision,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'get') {
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/approval${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching approvals from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
approvals: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'answer') {
|
||||
if (!approvalId) {
|
||||
logger.error('Missing approvalId in request')
|
||||
return NextResponse.json({ error: 'Approval ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const approvalIdValidation = validateAlphanumericId(approvalId, 'approvalId')
|
||||
if (!approvalIdValidation.isValid) {
|
||||
return NextResponse.json({ error: approvalIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const decisionValidation = validateEnum(decision, VALID_DECISIONS, 'decision')
|
||||
if (!decisionValidation.isValid) {
|
||||
return NextResponse.json({ error: decisionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/approval/${approvalId}`
|
||||
|
||||
logger.info('Answering approval:', { issueIdOrKey, approvalId, decision })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ decision }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
approvalId,
|
||||
decision,
|
||||
success: true,
|
||||
approval: data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in approvals operation:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
109
apps/sim/app/api/tools/jsm/comment/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
issueIdOrKey,
|
||||
body: commentBody,
|
||||
isPublic,
|
||||
} = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!commentBody) {
|
||||
logger.error('Missing comment body in request')
|
||||
return NextResponse.json({ error: 'Comment body is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/comment`
|
||||
|
||||
logger.info('Adding comment to:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({
|
||||
body: commentBody,
|
||||
public: isPublic ?? true,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
commentId: data.id,
|
||||
body: data.body,
|
||||
isPublic: data.public,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error adding comment:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
108
apps/sim/app/api/tools/jsm/comments/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
isPublic,
|
||||
internal,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (isPublic) params.append('public', isPublic)
|
||||
if (internal) params.append('internal', internal)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/comment${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching comments from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
comments: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching comments:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
155
apps/sim/app/api/tools/jsm/customers/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCustomersAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
query,
|
||||
start,
|
||||
limit,
|
||||
emails,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const parsedEmails = emails
|
||||
? typeof emails === 'string'
|
||||
? emails
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email)
|
||||
: emails
|
||||
: []
|
||||
|
||||
const isAddOperation = parsedEmails.length > 0
|
||||
|
||||
if (isAddOperation) {
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
|
||||
|
||||
logger.info('Adding customers to:', url, { emails: parsedEmails })
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
usernames: parsedEmails,
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
if (query) params.append('query', query)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching customers from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
customers: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with customers operation:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
171
apps/sim/app/api/tools/jsm/organization/route.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmOrganizationAPI')
|
||||
|
||||
const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
name,
|
||||
serviceDeskId,
|
||||
organizationId,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'create') {
|
||||
if (!name) {
|
||||
logger.error('Missing organization name in request')
|
||||
return NextResponse.json({ error: 'Organization name is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/organization`
|
||||
|
||||
logger.info('Creating organization:', { name })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
organizationId: data.id,
|
||||
name: data.name,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'add_to_service_desk') {
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!organizationId) {
|
||||
logger.error('Missing organizationId in request')
|
||||
return NextResponse.json({ error: 'Organization ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const organizationIdValidation = validateAlphanumericId(organizationId, 'organizationId')
|
||||
if (!organizationIdValidation.isValid) {
|
||||
return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization`
|
||||
|
||||
logger.info('Adding organization to service desk:', { serviceDeskId, organizationId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ organizationId: Number.parseInt(organizationId, 10) }),
|
||||
})
|
||||
|
||||
if (response.status === 204 || response.ok) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
organizationId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in organization operation:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
96
apps/sim/app/api/tools/jsm/organizations/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmOrganizationsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching organizations from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
organizations: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching organizations:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
176
apps/sim/app/api/tools/jsm/participants/route.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmParticipantsAPI')
|
||||
|
||||
const VALID_ACTIONS = ['get', 'add'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
issueIdOrKey,
|
||||
accountIds,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'get') {
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/participant${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching participants from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
participants: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'add') {
|
||||
if (!accountIds) {
|
||||
logger.error('Missing accountIds in request')
|
||||
return NextResponse.json({ error: 'Account IDs are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const parsedAccountIds =
|
||||
typeof accountIds === 'string'
|
||||
? accountIds
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter((id: string) => id)
|
||||
: accountIds
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/participant`
|
||||
|
||||
logger.info('Adding participants to:', url, { accountIds: parsedAccountIds })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ accountIds: parsedAccountIds }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
participants: data.values || [],
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in participants operation:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
105
apps/sim/app/api/tools/jsm/queues/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmQueuesAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
includeCount,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (includeCount) params.append('includeCount', includeCount)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching queues from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
queues: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching queues:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
169
apps/sim/app/api/tools/jsm/request/route.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
summary,
|
||||
description,
|
||||
raiseOnBehalfOf,
|
||||
requestFieldValues,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const isCreateOperation = serviceDeskId && requestTypeId && summary
|
||||
|
||||
if (isCreateOperation) {
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId')
|
||||
if (!requestTypeIdValidation.isValid) {
|
||||
return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const url = `${baseUrl}/request`
|
||||
|
||||
logger.info('Creating request at:', url)
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
requestFieldValues: requestFieldValues || {
|
||||
summary,
|
||||
...(description && { description }),
|
||||
},
|
||||
}
|
||||
|
||||
if (raiseOnBehalfOf) {
|
||||
requestBody.raiseOnBehalfOf = raiseOnBehalfOf
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueId: data.issueId,
|
||||
issueKey: data.issueKey,
|
||||
requestTypeId: data.requestTypeId,
|
||||
serviceDeskId: data.serviceDeskId,
|
||||
success: true,
|
||||
url: `https://${domain}/browse/${data.issueKey}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}`
|
||||
|
||||
logger.info('Fetching request from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
request: data,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with request operation:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
111
apps/sim/app/api/tools/jsm/requests/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
requestOwnership,
|
||||
requestStatus,
|
||||
searchTerm,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
if (serviceDeskId) {
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (serviceDeskId) params.append('serviceDeskId', serviceDeskId)
|
||||
if (requestOwnership && requestOwnership !== 'ALL_REQUESTS') {
|
||||
params.append('requestOwnership', requestOwnership)
|
||||
}
|
||||
if (requestStatus && requestStatus !== 'ALL') {
|
||||
params.append('requestStatus', requestStatus)
|
||||
}
|
||||
if (searchTerm) params.append('searchTerm', searchTerm)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching requests from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
requests: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching requests:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
96
apps/sim/app/api/tools/jsm/requesttypes/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestTypesAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching request types from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
requestTypes: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching request types:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
86
apps/sim/app/api/tools/jsm/servicedesks/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmServiceDesksAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching service desks from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDesks: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching service desks:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
97
apps/sim/app/api/tools/jsm/sla/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmSlaAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching SLA info from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
slas: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching SLA info:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
121
apps/sim/app/api/tools/jsm/transition/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
issueIdOrKey,
|
||||
transitionId,
|
||||
comment,
|
||||
} = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!transitionId) {
|
||||
logger.error('Missing transitionId in request')
|
||||
return NextResponse.json({ error: 'Transition ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const transitionIdValidation = validateAlphanumericId(transitionId, 'transitionId')
|
||||
if (!transitionIdValidation.isValid) {
|
||||
return NextResponse.json({ error: transitionIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
|
||||
logger.info('Transitioning request at:', url)
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
id: transitionId,
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
body.additionalComment = {
|
||||
body: comment,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
transitionId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error transitioning request:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
91
apps/sim/app/api/tools/jsm/transitions/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
|
||||
logger.info('Fetching transitions from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
transitions: data.values || [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching transitions:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import {
|
||||
createSchedulesForDeploy,
|
||||
@@ -160,6 +161,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
||||
|
||||
// Sync MCP tools with the latest parameter schema
|
||||
await syncMcpToolsForWorkflow({ workflowId: id, requestId, context: 'deploy' })
|
||||
|
||||
const responseApiKeyInfo = workflowData!.workspaceId
|
||||
? 'Workspace API keys'
|
||||
: 'Personal API keys'
|
||||
@@ -217,6 +221,9 @@ export async function DELETE(
|
||||
.where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Remove all MCP tools that reference this workflow
|
||||
await removeMcpToolsForWorkflow(id, requestId)
|
||||
|
||||
logger.info(`[${requestId}] Workflow undeployed successfully: ${id}`)
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
@@ -31,6 +32,18 @@ export async function POST(
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// Get the state of the version being activated for MCP tool sync
|
||||
const [versionData] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.version, versionNum)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(workflowDeploymentVersion)
|
||||
@@ -65,6 +78,16 @@ export async function POST(
|
||||
await tx.update(workflow).set(updateData).where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Sync MCP tools with the activated version's parameter schema
|
||||
if (versionData?.state) {
|
||||
await syncMcpToolsForWorkflow({
|
||||
workflowId: id,
|
||||
requestId,
|
||||
state: versionData.state,
|
||||
context: 'activate',
|
||||
})
|
||||
}
|
||||
|
||||
return createSuccessResponse({ success: true, deployedAt: now })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error activating deployment for workflow: ${id}`, error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
@@ -87,6 +88,14 @@ export async function POST(
|
||||
.set({ lastSynced: new Date(), updatedAt: new Date() })
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
// Sync MCP tools with the reverted version's parameter schema
|
||||
await syncMcpToolsForWorkflow({
|
||||
workflowId: id,
|
||||
requestId,
|
||||
state: deployedState,
|
||||
context: 'revert',
|
||||
})
|
||||
|
||||
try {
|
||||
const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
await fetch(`${socketServerUrl}/api/workflow-reverted`, {
|
||||
|
||||
@@ -109,7 +109,7 @@ type AsyncExecutionParams = {
|
||||
workflowId: string
|
||||
userId: string
|
||||
input: any
|
||||
triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
|
||||
triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,14 +252,15 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
})
|
||||
|
||||
const executionId = uuidv4()
|
||||
type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
|
||||
type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
|
||||
let loggingTriggerType: LoggingTriggerType = 'manual'
|
||||
if (
|
||||
triggerType === 'api' ||
|
||||
triggerType === 'chat' ||
|
||||
triggerType === 'webhook' ||
|
||||
triggerType === 'schedule' ||
|
||||
triggerType === 'manual'
|
||||
triggerType === 'manual' ||
|
||||
triggerType === 'mcp'
|
||||
) {
|
||||
loggingTriggerType = triggerType as LoggingTriggerType
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import type { ChangelogEntry } from '@/app/changelog/components/changelog-content'
|
||||
|
||||
@@ -41,7 +41,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Workspace layout dimensions: set CSS vars before hydration to avoid layout jump */}
|
||||
{/*
|
||||
Workspace layout dimensions: set CSS vars before hydration to avoid layout jump.
|
||||
|
||||
IMPORTANT: These hardcoded values must stay in sync with stores/constants.ts
|
||||
We cannot use imports here since this is a blocking script that runs before React.
|
||||
*/}
|
||||
<script
|
||||
id='workspace-layout-dimensions'
|
||||
dangerouslySetInnerHTML={{
|
||||
@@ -84,7 +89,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
var panelWidth = panelState && panelState.panelWidth;
|
||||
var maxPanelWidth = window.innerWidth * 0.4;
|
||||
|
||||
if (panelWidth >= 260 && panelWidth <= maxPanelWidth) {
|
||||
if (panelWidth >= 290 && panelWidth <= maxPanelWidth) {
|
||||
document.documentElement.style.setProperty('--panel-width', panelWidth + 'px');
|
||||
} else if (panelWidth > maxPanelWidth) {
|
||||
document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px');
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Bell, Folder, Key, Settings, User } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ArrowLeft, Bell, Folder, Key, Moon, Settings, Sun, User } from 'lucide-react'
|
||||
import { notFound, useRouter } from 'next/navigation'
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
BubbleChatPreview,
|
||||
Button,
|
||||
Card as CardIcon,
|
||||
Checkbox,
|
||||
ChevronDown,
|
||||
Code,
|
||||
Combobox,
|
||||
@@ -50,6 +54,7 @@ import {
|
||||
PopoverTrigger,
|
||||
Redo,
|
||||
Rocket,
|
||||
Slider,
|
||||
SModal,
|
||||
SModalContent,
|
||||
SModalMain,
|
||||
@@ -62,6 +67,12 @@ import {
|
||||
SModalSidebarSectionTitle,
|
||||
SModalTrigger,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
Trash,
|
||||
@@ -112,7 +123,19 @@ export default function PlaygroundPage() {
|
||||
const router = useRouter()
|
||||
const [comboboxValue, setComboboxValue] = useState('')
|
||||
const [switchValue, setSwitchValue] = useState(false)
|
||||
const [checkboxValue, setCheckboxValue] = useState(false)
|
||||
const [sliderValue, setSliderValue] = useState([50])
|
||||
const [activeTab, setActiveTab] = useState('profile')
|
||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode(!isDarkMode)
|
||||
document.documentElement.classList.toggle('dark')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||
}, [])
|
||||
|
||||
if (!isTruthy(env.NEXT_PUBLIC_ENABLE_PLAYGROUND)) {
|
||||
notFound()
|
||||
@@ -121,18 +144,26 @@ export default function PlaygroundPage() {
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<div className='relative min-h-screen bg-[var(--bg)] p-8'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => router.back()}
|
||||
className='absolute top-8 left-8 h-8 w-8 p-0'
|
||||
>
|
||||
<ArrowLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Go back</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div className='absolute top-8 left-8 flex items-center gap-2'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button variant='ghost' onClick={() => router.back()} className='h-8 w-8 p-0'>
|
||||
<ArrowLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Go back</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<div className='absolute top-8 right-8'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button variant='default' onClick={toggleDarkMode} className='h-8 w-8 p-0'>
|
||||
{isDarkMode ? <Sun className='h-4 w-4' /> : <Moon className='h-4 w-4' />}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{isDarkMode ? 'Light mode' : 'Dark mode'}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<div className='mx-auto max-w-4xl space-y-12'>
|
||||
<div>
|
||||
<h1 className='font-semibold text-2xl text-[var(--text-primary)]'>
|
||||
@@ -185,6 +216,50 @@ export default function PlaygroundPage() {
|
||||
<VariantRow label='outline'>
|
||||
<Badge variant='outline'>Outline</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='green'>
|
||||
<Badge variant='green'>Green</Badge>
|
||||
<Badge variant='green' dot>
|
||||
With Dot
|
||||
</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='red'>
|
||||
<Badge variant='red'>Red</Badge>
|
||||
<Badge variant='red' dot>
|
||||
With Dot
|
||||
</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='blue'>
|
||||
<Badge variant='blue'>Blue</Badge>
|
||||
<Badge variant='blue' dot>
|
||||
With Dot
|
||||
</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='blue-secondary'>
|
||||
<Badge variant='blue-secondary'>Blue Secondary</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='purple'>
|
||||
<Badge variant='purple'>Purple</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='orange'>
|
||||
<Badge variant='orange'>Orange</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='amber'>
|
||||
<Badge variant='amber'>Amber</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='teal'>
|
||||
<Badge variant='teal'>Teal</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='gray'>
|
||||
<Badge variant='gray'>Gray</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='gray-secondary'>
|
||||
<Badge variant='gray-secondary'>Gray Secondary</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='sizes'>
|
||||
<Badge size='sm'>Small</Badge>
|
||||
<Badge size='md'>Medium</Badge>
|
||||
<Badge size='lg'>Large</Badge>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Input */}
|
||||
@@ -220,6 +295,143 @@ export default function PlaygroundPage() {
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Checkbox */}
|
||||
<Section title='Checkbox'>
|
||||
<VariantRow label='default'>
|
||||
<Checkbox checked={checkboxValue} onCheckedChange={(c) => setCheckboxValue(!!c)} />
|
||||
<span className='text-[var(--text-secondary)] text-sm'>
|
||||
{checkboxValue ? 'Checked' : 'Unchecked'}
|
||||
</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='size sm'>
|
||||
<Checkbox size='sm' />
|
||||
<span className='text-[var(--text-secondary)] text-sm'>Small (14px)</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='size md'>
|
||||
<Checkbox size='md' />
|
||||
<span className='text-[var(--text-secondary)] text-sm'>Medium (16px)</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='size lg'>
|
||||
<Checkbox size='lg' />
|
||||
<span className='text-[var(--text-secondary)] text-sm'>Large (20px)</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='disabled'>
|
||||
<Checkbox disabled />
|
||||
<Checkbox disabled checked />
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Slider */}
|
||||
<Section title='Slider'>
|
||||
<VariantRow label='default'>
|
||||
<div className='w-48'>
|
||||
<Slider value={sliderValue} onValueChange={setSliderValue} max={100} step={1} />
|
||||
</div>
|
||||
<span className='text-[var(--text-secondary)] text-sm'>{sliderValue[0]}</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='disabled'>
|
||||
<div className='w-48'>
|
||||
<Slider value={[30]} disabled max={100} step={1} />
|
||||
</div>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Avatar */}
|
||||
<Section title='Avatar'>
|
||||
<VariantRow label='sizes'>
|
||||
<Avatar size='xs'>
|
||||
<AvatarFallback>XS</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='sm'>
|
||||
<AvatarFallback>SM</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='md'>
|
||||
<AvatarFallback>MD</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='lg'>
|
||||
<AvatarFallback>LG</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='xl'>
|
||||
<AvatarFallback>XL</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='with image'>
|
||||
<Avatar size='md'>
|
||||
<AvatarImage src='https://github.com/shadcn.png' alt='User' />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='status online'>
|
||||
<Avatar size='md' status='online'>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='status offline'>
|
||||
<Avatar size='md' status='offline'>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='status busy'>
|
||||
<Avatar size='md' status='busy'>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='status away'>
|
||||
<Avatar size='md' status='away'>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
<VariantRow label='all sizes with status'>
|
||||
<Avatar size='xs' status='online'>
|
||||
<AvatarFallback>XS</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='sm' status='online'>
|
||||
<AvatarFallback>SM</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='md' status='online'>
|
||||
<AvatarFallback>MD</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='lg' status='online'>
|
||||
<AvatarFallback>LG</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size='xl' status='online'>
|
||||
<AvatarFallback>XL</AvatarFallback>
|
||||
</Avatar>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Table */}
|
||||
<Section title='Table'>
|
||||
<VariantRow label='default'>
|
||||
<Table className='max-w-md'>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow className='hover:bg-[var(--surface-2)]'>
|
||||
<TableCell>Alice</TableCell>
|
||||
<TableCell>Active</TableCell>
|
||||
<TableCell>Admin</TableCell>
|
||||
</TableRow>
|
||||
<TableRow className='hover:bg-[var(--surface-2)]'>
|
||||
<TableCell>Bob</TableCell>
|
||||
<TableCell>Pending</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
</TableRow>
|
||||
<TableRow className='hover:bg-[var(--surface-2)]'>
|
||||
<TableCell>Charlie</TableCell>
|
||||
<TableCell>Active</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Combobox */}
|
||||
<Section title='Combobox'>
|
||||
<VariantRow label='default'>
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileText,
|
||||
Play,
|
||||
RefreshCw,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge, Button, Textarea } from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
@@ -25,10 +13,8 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
import type { ResumeStatus } from '@/executor/types'
|
||||
|
||||
@@ -119,27 +105,14 @@ interface ResumeExecutionPageProps {
|
||||
initialContextId?: string | null
|
||||
}
|
||||
|
||||
const RESUME_STATUS_STYLES: Record<string, { style: string; icon: React.ReactNode }> = {
|
||||
paused: {
|
||||
style: 'border-amber-200 bg-amber-50 text-amber-700',
|
||||
icon: <Clock className='h-3 w-3' />,
|
||||
},
|
||||
queued: {
|
||||
style: 'border-blue-200 bg-blue-50 text-blue-700',
|
||||
icon: <Clock className='h-3 w-3' />,
|
||||
},
|
||||
resuming: {
|
||||
style: 'border-indigo-200 bg-indigo-50 text-indigo-700',
|
||||
icon: <Play className='h-3 w-3' />,
|
||||
},
|
||||
resumed: {
|
||||
style: 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
||||
icon: <CheckCircle2 className='h-3 w-3' />,
|
||||
},
|
||||
failed: {
|
||||
style: 'border-red-200 bg-red-50 text-red-700',
|
||||
icon: <XCircle className='h-3 w-3' />,
|
||||
},
|
||||
const RESUME_STATUS_STYLES: Record<string, string> = {
|
||||
paused: 'border-[var(--c-F59E0B)]/30 bg-[var(--c-F59E0B)]/10 text-[var(--c-F59E0B)]',
|
||||
queued:
|
||||
'border-[var(--brand-tertiary)]/30 bg-[var(--brand-tertiary)]/10 text-[var(--brand-tertiary)]',
|
||||
resuming:
|
||||
'border-[var(--brand-primary)]/30 bg-[var(--brand-primary)]/10 text-[var(--brand-primary)]',
|
||||
resumed: 'border-[var(--text-success)]/30 bg-[var(--text-success)]/10 text-[var(--text-success)]',
|
||||
failed: 'border-[var(--text-error)]/30 bg-[var(--text-error)]/10 text-[var(--text-error)]',
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
@@ -157,13 +130,11 @@ function getStatusLabel(status: string): string {
|
||||
}
|
||||
|
||||
function ResumeStatusBadge({ status }: { status: string }) {
|
||||
const config = RESUME_STATUS_STYLES[status] ?? {
|
||||
style: 'border-slate-200 bg-slate-100 text-slate-700',
|
||||
icon: <AlertCircle className='h-3 w-3' />,
|
||||
}
|
||||
const style =
|
||||
RESUME_STATUS_STYLES[status] ??
|
||||
'border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-secondary)]'
|
||||
return (
|
||||
<Badge variant='outline' className={`${config.style} flex items-center gap-1.5`}>
|
||||
{config.icon}
|
||||
<Badge variant='outline' className={cn(style)}>
|
||||
{getStatusLabel(status)}
|
||||
</Badge>
|
||||
)
|
||||
@@ -418,7 +389,7 @@ export default function ResumeExecutionPage({
|
||||
value={selectValue}
|
||||
onValueChange={(val) => handleFormFieldChange(field.name, val)}
|
||||
>
|
||||
<SelectTrigger className='w-full rounded-[12px] border-slate-200'>
|
||||
<SelectTrigger className='w-full rounded-[6px] border-[var(--border)] bg-[var(--surface-1)]'>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.required ? 'Select true or false' : 'Select true, false, or leave blank'
|
||||
@@ -440,7 +411,6 @@ export default function ResumeExecutionPage({
|
||||
value={value}
|
||||
onChange={(event) => handleFormFieldChange(field.name, event.target.value)}
|
||||
placeholder={field.placeholder ?? 'Enter a number...'}
|
||||
className='rounded-[12px] border-slate-200'
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
@@ -451,7 +421,7 @@ export default function ResumeExecutionPage({
|
||||
value={value}
|
||||
onChange={(event) => handleFormFieldChange(field.name, event.target.value)}
|
||||
placeholder={field.placeholder ?? (field.type === 'array' ? '[...]' : '{...}')}
|
||||
className='min-h-[120px] rounded-[12px] border-slate-200 font-mono text-sm'
|
||||
className='min-h-[120px] font-mono text-[13px]'
|
||||
/>
|
||||
)
|
||||
default: {
|
||||
@@ -462,7 +432,7 @@ export default function ResumeExecutionPage({
|
||||
value={value}
|
||||
onChange={(event) => handleFormFieldChange(field.name, event.target.value)}
|
||||
placeholder={field.placeholder ?? 'Enter value...'}
|
||||
className='min-h-[120px] rounded-[12px] border-slate-200'
|
||||
className='min-h-[120px]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -471,7 +441,6 @@ export default function ResumeExecutionPage({
|
||||
value={value}
|
||||
onChange={(event) => handleFormFieldChange(field.name, event.target.value)}
|
||||
placeholder={field.placeholder ?? 'Enter value...'}
|
||||
className='rounded-[12px] border-slate-200'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -518,18 +487,6 @@ export default function ResumeExecutionPage({
|
||||
.filter((row): row is ResponseStructureRow => row !== null)
|
||||
}, [selectedDetail])
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
const hadDark = root.classList.contains('dark')
|
||||
const hadLight = root.classList.contains('light')
|
||||
root.classList.add('light')
|
||||
root.classList.remove('dark')
|
||||
return () => {
|
||||
if (!hadLight) root.classList.remove('light')
|
||||
if (hadDark) root.classList.add('dark')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedContextId) {
|
||||
setSelectedDetail(null)
|
||||
@@ -907,10 +864,10 @@ export default function ResumeExecutionPage({
|
||||
|
||||
const statusDetailNode = useMemo(() => {
|
||||
return (
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<div className='flex flex-wrap items-center gap-[8px]'>
|
||||
<ResumeStatusBadge status={selectedStatus} />
|
||||
{queuePosition && queuePosition > 0 ? (
|
||||
<span className={`${inter.className} text-slate-500 text-xs`}>
|
||||
<span className='text-[12px] text-[var(--text-muted)]'>
|
||||
Queue position {queuePosition}
|
||||
</span>
|
||||
) : null}
|
||||
@@ -931,33 +888,23 @@ export default function ResumeExecutionPage({
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
}}
|
||||
className={`group w-full rounded-lg border p-3 text-left transition-all duration-150 hover:border-slate-300 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500/40 ${
|
||||
isSelected
|
||||
? 'border-blue-500 bg-blue-50/50 shadow-sm'
|
||||
: subdued
|
||||
? 'border-slate-200 bg-slate-50 opacity-75'
|
||||
: 'border-slate-200 bg-white'
|
||||
}`}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] border border-[var(--border)] bg-[var(--surface-1)] p-[12px] text-left',
|
||||
'hover:bg-[var(--surface-3)] focus:outline-none',
|
||||
isSelected && 'bg-[var(--surface-3)]'
|
||||
)}
|
||||
>
|
||||
<div className='flex items-start justify-between gap-2'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<p className='truncate font-medium text-slate-900 text-sm'>{pause.contextId}</p>
|
||||
</div>
|
||||
<div className='mt-1.5 flex items-center gap-1 text-slate-500 text-xs'>
|
||||
<Calendar className='h-3 w-3' />
|
||||
<span>{formatDate(pause.registeredAt)}</span>
|
||||
</div>
|
||||
{pause.queuePosition != null && pause.queuePosition > 0 && (
|
||||
<div className='mt-1 flex items-center gap-1 text-amber-600 text-xs'>
|
||||
<Clock className='h-3 w-3' />
|
||||
<span>Position {pause.queuePosition}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-shrink-0'>
|
||||
<ResumeStatusBadge status={pause.resumeStatus} />
|
||||
</div>
|
||||
<div className='mb-[8px] flex items-center justify-between'>
|
||||
<ResumeStatusBadge status={pause.resumeStatus} />
|
||||
<span className='truncate font-medium text-[11px] text-[var(--text-primary)]'>
|
||||
{pause.contextId}
|
||||
</span>
|
||||
</div>
|
||||
<div className='space-y-[4px] text-[11px] text-[var(--text-secondary)]'>
|
||||
<p>Registered: {formatDate(pause.registeredAt)}</p>
|
||||
{pause.queuePosition != null && pause.queuePosition > 0 && (
|
||||
<p className='text-[var(--c-F59E0B)]'>Queue Position: {pause.queuePosition}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
@@ -996,41 +943,37 @@ export default function ResumeExecutionPage({
|
||||
|
||||
if (!executionDetail) {
|
||||
return (
|
||||
<div className='relative min-h-screen'>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
|
||||
<div className='relative min-h-screen bg-[var(--bg)]'>
|
||||
<Nav variant='auth' />
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-[16px]'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1
|
||||
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||
>
|
||||
<div className='space-y-[4px] text-center'>
|
||||
<h1 className='font-medium text-[24px] text-[var(--text-primary)] tracking-tight'>
|
||||
Execution Not Found
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
<p className='text-[14px] text-[var(--text-secondary)]'>
|
||||
The execution you are trying to resume could not be located or has already
|
||||
completed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 w-full space-y-3'>
|
||||
<div className='mt-[24px] w-full space-y-[12px]'>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={() => router.push('/')}
|
||||
className='auth-button-gradient flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
|
||||
variant='tertiary'
|
||||
className='w-full'
|
||||
>
|
||||
Return Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
|
||||
>
|
||||
<div className='fixed right-0 bottom-0 left-0 z-50 pb-[32px] text-center text-[13px] text-[var(--text-muted)]'>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
className='text-[var(--text-secondary)] underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
@@ -1043,106 +986,90 @@ export default function ResumeExecutionPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative min-h-screen bg-gradient-to-b from-slate-50 to-white'>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
|
||||
<div className='relative min-h-screen bg-[var(--bg)]'>
|
||||
<Nav variant='auth' />
|
||||
<div className='mx-auto min-h-[calc(100vh-120px)] max-w-7xl px-4 py-6 sm:py-8'>
|
||||
<div className='mx-auto min-h-[calc(100vh-120px)] max-w-7xl px-[24px] py-[24px]'>
|
||||
{/* Header Section */}
|
||||
<div className='mb-6'>
|
||||
<div className='mb-[24px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<h1
|
||||
className={`${soehne.className} font-semibold text-3xl text-slate-900 tracking-tight`}
|
||||
>
|
||||
<h1 className='font-medium text-[18px] text-[var(--text-primary)] tracking-tight'>
|
||||
Paused Execution
|
||||
</h1>
|
||||
<p className={`${inter.className} mt-1 text-slate-600 text-sm`}>
|
||||
<p className='mt-[4px] text-[13px] text-[var(--text-secondary)]'>
|
||||
Review and manage execution pause points
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={refreshExecutionDetail}
|
||||
disabled={refreshingExecution}
|
||||
className='gap-2'
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${refreshingExecution ? 'animate-spin' : ''}`} />
|
||||
{refreshingExecution ? 'Refreshing' : 'Refresh'}
|
||||
{refreshingExecution ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className='mb-6 grid grid-cols-1 gap-4 sm:grid-cols-3'>
|
||||
<Card>
|
||||
<CardContent className='p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<p className='font-medium text-slate-600 text-sm'>Total Pauses</p>
|
||||
<p className='mt-2 font-semibold text-3xl text-slate-900'>{totalPauses}</p>
|
||||
</div>
|
||||
<div className='rounded-full bg-blue-100 p-3'>
|
||||
<Clock className='h-6 w-6 text-blue-600' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='mb-[24px] grid grid-cols-1 gap-[16px] sm:grid-cols-3'>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Total Pauses
|
||||
</span>
|
||||
</div>
|
||||
<div className='px-[16px] py-[16px]'>
|
||||
<p className='font-semibold text-[28px] text-[var(--text-primary)]'>{totalPauses}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className='p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<p className='font-medium text-slate-600 text-sm'>Resumed</p>
|
||||
<p className='mt-2 font-semibold text-3xl text-emerald-600'>{resumedCount}</p>
|
||||
</div>
|
||||
<div className='rounded-full bg-emerald-100 p-3'>
|
||||
<CheckCircle2 className='h-6 w-6 text-emerald-600' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>Resumed</span>
|
||||
</div>
|
||||
<div className='px-[16px] py-[16px]'>
|
||||
<p className='font-semibold text-[28px] text-[var(--text-success)]'>{resumedCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className='p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<p className='font-medium text-slate-600 text-sm'>Pending</p>
|
||||
<p className='mt-2 font-semibold text-3xl text-amber-600'>{pendingCount}</p>
|
||||
</div>
|
||||
<div className='rounded-full bg-amber-100 p-3'>
|
||||
<AlertCircle className='h-6 w-6 text-amber-600' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>Pending</span>
|
||||
</div>
|
||||
<div className='px-[16px] py-[16px]'>
|
||||
<p className='font-semibold text-[28px] text-[var(--c-F59E0B)]'>{pendingCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-3'>
|
||||
<div className='grid grid-cols-1 gap-[24px] lg:grid-cols-3'>
|
||||
{/* Left Column: Pause Points + History */}
|
||||
<div className='space-y-6 lg:col-span-1'>
|
||||
<div className='space-y-[24px] lg:col-span-1'>
|
||||
{/* Pause Points List */}
|
||||
<Card className='h-fit'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Pause Points</CardTitle>
|
||||
<CardDescription>Select a pause point to view details</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='flex h-fit flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[12px]'>
|
||||
<h2 className='font-medium text-[14px] text-[var(--text-primary)]'>Pause Points</h2>
|
||||
<p className='mt-[2px] text-[12px] text-[var(--text-muted)]'>
|
||||
Select a pause point to view details
|
||||
</p>
|
||||
</div>
|
||||
<div className='space-y-[12px] p-[16px]'>
|
||||
{groupedPausePoints.active.length === 0 &&
|
||||
groupedPausePoints.resolved.length === 0 ? (
|
||||
<div className='flex flex-col items-center justify-center py-8 text-center'>
|
||||
<Clock className='mb-3 h-12 w-12 text-slate-300' />
|
||||
<p className='text-slate-500 text-sm'>No pause points found</p>
|
||||
<div className='flex flex-col items-center justify-center py-[32px] text-center'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
No pause points found
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groupedPausePoints.active.length > 0 && (
|
||||
<div className='space-y-3'>
|
||||
<h3 className='font-semibold text-slate-500 text-xs uppercase tracking-wider'>
|
||||
<div className='space-y-[12px]'>
|
||||
<h3 className='font-medium text-[11px] text-[var(--text-muted)] uppercase tracking-wider'>
|
||||
Active
|
||||
</h3>
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
{groupedPausePoints.active.map((pause) => renderPausePointCard(pause))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1150,12 +1077,14 @@ export default function ResumeExecutionPage({
|
||||
|
||||
{groupedPausePoints.resolved.length > 0 && (
|
||||
<>
|
||||
{groupedPausePoints.active.length > 0 && <Separator className='my-4' />}
|
||||
<div className='space-y-3'>
|
||||
<h3 className='font-semibold text-slate-500 text-xs uppercase tracking-wider'>
|
||||
{groupedPausePoints.active.length > 0 && (
|
||||
<Separator className='my-[16px]' />
|
||||
)}
|
||||
<div className='space-y-[12px]'>
|
||||
<h3 className='font-medium text-[11px] text-[var(--text-muted)] uppercase tracking-wider'>
|
||||
Completed
|
||||
</h3>
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
{groupedPausePoints.resolved.map((pause) =>
|
||||
renderPausePointCard(pause, true)
|
||||
)}
|
||||
@@ -1165,99 +1094,101 @@ export default function ResumeExecutionPage({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History */}
|
||||
{selectedDetail && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Resume History</CardTitle>
|
||||
<CardDescription>Previous resume attempts for this pause</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[12px]'>
|
||||
<h2 className='font-medium text-[14px] text-[var(--text-primary)]'>
|
||||
Resume History
|
||||
</h2>
|
||||
<p className='mt-[2px] text-[12px] text-[var(--text-muted)]'>
|
||||
Previous resume attempts for this pause
|
||||
</p>
|
||||
</div>
|
||||
<div className='p-[16px]'>
|
||||
{selectedDetail.queue.length > 0 ? (
|
||||
<div className='space-y-3'>
|
||||
<div className='space-y-[12px]'>
|
||||
{selectedDetail.queue.map((entry) => {
|
||||
const normalizedStatus = entry.status?.toLowerCase?.() ?? entry.status
|
||||
return (
|
||||
<div key={entry.id} className='rounded-lg border bg-white p-3'>
|
||||
<div className='flex items-start justify-between gap-2'>
|
||||
<div className='space-y-2'>
|
||||
<ResumeStatusBadge status={normalizedStatus} />
|
||||
<div className='space-y-1 text-slate-600 text-xs'>
|
||||
<p>
|
||||
ID:{' '}
|
||||
<span className='font-medium text-slate-900'>
|
||||
{entry.newExecutionId}
|
||||
</span>
|
||||
</p>
|
||||
{entry.claimedAt && <p>Started: {formatDate(entry.claimedAt)}</p>}
|
||||
{entry.completedAt && (
|
||||
<p>Completed: {formatDate(entry.completedAt)}</p>
|
||||
)}
|
||||
</div>
|
||||
{entry.failureReason && (
|
||||
<div className='mt-2 rounded border border-red-200 bg-red-50 p-2 text-red-700 text-xs'>
|
||||
{entry.failureReason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className='text-slate-400 text-xs'>
|
||||
{formatDate(entry.queuedAt)}
|
||||
<div
|
||||
key={entry.id}
|
||||
className='rounded-[6px] border border-[var(--border)] bg-[var(--surface-1)] p-[12px]'
|
||||
>
|
||||
<div className='mb-[8px] flex items-center justify-between'>
|
||||
<ResumeStatusBadge status={normalizedStatus} />
|
||||
<span className='font-medium text-[11px] text-[var(--text-primary)]'>
|
||||
{entry.newExecutionId}
|
||||
</span>
|
||||
</div>
|
||||
<div className='space-y-[4px] text-[11px] text-[var(--text-secondary)]'>
|
||||
<p>Queued: {formatDate(entry.queuedAt)}</p>
|
||||
{entry.claimedAt && (
|
||||
<p>Execution Started: {formatDate(entry.claimedAt)}</p>
|
||||
)}
|
||||
{entry.completedAt && (
|
||||
<p>Execution Completed: {formatDate(entry.completedAt)}</p>
|
||||
)}
|
||||
</div>
|
||||
{entry.failureReason && (
|
||||
<p className='mt-[8px] text-[11px] text-[var(--text-error)]'>
|
||||
{entry.failureReason}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col items-center justify-center py-8 text-center'>
|
||||
<Clock className='mb-3 h-8 w-8 text-slate-300' />
|
||||
<p className='text-slate-500 text-sm'>No resume attempts yet</p>
|
||||
<div className='flex flex-col items-center justify-center py-[32px] text-center'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
No resume attempts yet
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column: Content + Input */}
|
||||
<div className='space-y-6 lg:col-span-2'>
|
||||
<div className='space-y-[24px] lg:col-span-2'>
|
||||
{loadingDetail && !selectedDetail ? (
|
||||
<Card>
|
||||
<CardContent className='flex h-64 items-center justify-center p-6'>
|
||||
<div className='text-center'>
|
||||
<RefreshCw className='mx-auto mb-3 h-8 w-8 animate-spin text-slate-400' />
|
||||
<p className='text-slate-500 text-sm'>Loading pause details...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='flex h-[256px] items-center justify-center p-[24px]'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
Loading pause details...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedContextId ? (
|
||||
<Card>
|
||||
<CardContent className='flex h-64 items-center justify-center p-6'>
|
||||
<div className='text-center'>
|
||||
<FileText className='mx-auto mb-3 h-12 w-12 text-slate-300' />
|
||||
<p className='text-slate-500 text-sm'>Select a pause point to view details</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='flex h-[256px] items-center justify-center p-[24px]'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
Select a pause point to view details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedDetail ? (
|
||||
<Card>
|
||||
<CardContent className='flex h-64 items-center justify-center p-6'>
|
||||
<div className='text-center'>
|
||||
<XCircle className='mx-auto mb-3 h-12 w-12 text-red-300' />
|
||||
<p className='text-slate-500 text-sm'>Pause details could not be loaded</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='flex h-[256px] items-center justify-center p-[24px]'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
Pause details could not be loaded
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header with Status */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<h2 className='font-semibold text-2xl text-slate-900'>Pause Details</h2>
|
||||
<p className='mt-1 text-slate-600 text-sm'>
|
||||
<h2 className='font-medium text-[16px] text-[var(--text-primary)]'>
|
||||
Pause Details
|
||||
</h2>
|
||||
<p className='mt-[4px] text-[13px] text-[var(--text-secondary)]'>
|
||||
Review content and provide input to resume
|
||||
</p>
|
||||
</div>
|
||||
@@ -1266,75 +1197,81 @@ export default function ResumeExecutionPage({
|
||||
|
||||
{/* Active Resume Entry Alert */}
|
||||
{selectedDetail.activeResumeEntry && (
|
||||
<Card className='border-blue-200 bg-blue-50/50'>
|
||||
<CardHeader>
|
||||
<div className='flex items-center justify-between'>
|
||||
<CardTitle className='text-blue-900 text-sm'>
|
||||
Current Resume Attempt
|
||||
</CardTitle>
|
||||
<ResumeStatusBadge
|
||||
status={
|
||||
selectedDetail.activeResumeEntry.status?.toLowerCase?.() ??
|
||||
selectedDetail.activeResumeEntry.status
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-2 text-blue-800 text-sm'>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--brand-tertiary)]/30 bg-[var(--brand-tertiary)]/5'>
|
||||
<div className='flex items-center justify-between bg-[var(--brand-tertiary)]/10 px-[16px] py-[10px]'>
|
||||
<h3 className='font-medium text-[13px] text-[var(--brand-tertiary)]'>
|
||||
Current Resume Attempt
|
||||
</h3>
|
||||
<ResumeStatusBadge
|
||||
status={
|
||||
selectedDetail.activeResumeEntry.status?.toLowerCase?.() ??
|
||||
selectedDetail.activeResumeEntry.status
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-[4px] p-[16px] text-[13px] text-[var(--text-secondary)]'>
|
||||
<p>
|
||||
Resume execution ID:{' '}
|
||||
<span className='font-medium'>
|
||||
Execution ID:{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>
|
||||
{selectedDetail.activeResumeEntry.newExecutionId}
|
||||
</span>
|
||||
</p>
|
||||
{selectedDetail.activeResumeEntry.claimedAt && (
|
||||
<p>Started at {formatDate(selectedDetail.activeResumeEntry.claimedAt)}</p>
|
||||
<p>
|
||||
Execution Started:{' '}
|
||||
{formatDate(selectedDetail.activeResumeEntry.claimedAt)}
|
||||
</p>
|
||||
)}
|
||||
{selectedDetail.activeResumeEntry.completedAt && (
|
||||
<p>
|
||||
Completed at {formatDate(selectedDetail.activeResumeEntry.completedAt)}
|
||||
Execution Completed:{' '}
|
||||
{formatDate(selectedDetail.activeResumeEntry.completedAt)}
|
||||
</p>
|
||||
)}
|
||||
{selectedDetail.activeResumeEntry.failureReason && (
|
||||
<div className='mt-2 rounded border border-red-300 bg-red-100 p-3 text-red-800 text-sm'>
|
||||
<p className='mt-[8px] text-[12px] text-[var(--text-error)]'>
|
||||
{selectedDetail.activeResumeEntry.failureReason}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Section */}
|
||||
{responseStructureRows.length > 0 ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-sm'>Content</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<table className='min-w-full divide-y divide-slate-200'>
|
||||
<thead className='bg-slate-50'>
|
||||
<tr>
|
||||
<th className='px-4 py-2 text-left font-medium text-slate-600 text-xs'>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<h3 className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Content
|
||||
</h3>
|
||||
</div>
|
||||
<div className='p-[16px]'>
|
||||
<div className='overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<table className='min-w-full divide-y divide-[var(--border)]'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b bg-[var(--surface-3)]'>
|
||||
<th className='px-[16px] py-[8px] text-left font-medium text-[11px] text-[var(--text-secondary)]'>
|
||||
Field
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left font-medium text-slate-600 text-xs'>
|
||||
<th className='px-[16px] py-[8px] text-left font-medium text-[11px] text-[var(--text-secondary)]'>
|
||||
Type
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left font-medium text-slate-600 text-xs'>
|
||||
<th className='px-[16px] py-[8px] text-left font-medium text-[11px] text-[var(--text-secondary)]'>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='divide-y divide-slate-200 bg-white'>
|
||||
<tbody className='divide-y divide-[var(--border)]'>
|
||||
{responseStructureRows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td className='px-4 py-2 font-medium text-slate-800 text-sm'>
|
||||
<td className='px-[16px] py-[8px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{row.name}
|
||||
</td>
|
||||
<td className='px-4 py-2 text-slate-500 text-sm'>{row.type}</td>
|
||||
<td className='px-4 py-2'>
|
||||
<pre className='max-h-32 overflow-auto whitespace-pre-wrap break-words font-mono text-slate-700 text-xs'>
|
||||
<td className='px-[16px] py-[8px] text-[13px] text-[var(--text-secondary)]'>
|
||||
{row.type}
|
||||
</td>
|
||||
<td className='px-[16px] py-[8px]'>
|
||||
<pre className='max-h-[128px] overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] text-[var(--text-secondary)]'>
|
||||
{formatStructureValue(row.value)}
|
||||
</pre>
|
||||
</td>
|
||||
@@ -1343,57 +1280,69 @@ export default function ResumeExecutionPage({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-sm'>Pause Response Data</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className='max-h-60 overflow-auto rounded-lg bg-slate-900 p-4 font-mono text-slate-100 text-xs'>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<h3 className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Pause Response Data
|
||||
</h3>
|
||||
</div>
|
||||
<div className='p-[16px]'>
|
||||
<pre className='max-h-[240px] overflow-auto rounded-[6px] bg-[#1e1e1e] p-[16px] font-mono text-[#d4d4d4] text-[12px]'>
|
||||
{pauseResponsePreview}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Section */}
|
||||
{isHumanMode && hasInputFormat ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-sm'>Resume Form</CardTitle>
|
||||
<CardDescription>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<h3 className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Resume Form
|
||||
</h3>
|
||||
<p className='mt-[2px] text-[12px] text-[var(--text-muted)]'>
|
||||
Fill out the required fields to resume execution
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
</p>
|
||||
</div>
|
||||
<div className='space-y-[16px] p-[16px]'>
|
||||
{inputFormatFields.map((field) => (
|
||||
<div key={field.id} className='space-y-2'>
|
||||
<Label className='font-medium text-slate-700 text-sm'>
|
||||
<div key={field.id} className='space-y-[6px]'>
|
||||
<Label className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{field.label}
|
||||
{field.required && <span className='ml-1 text-red-500'>*</span>}
|
||||
{field.required && (
|
||||
<span className='ml-[4px] text-[var(--text-error)]'>*</span>
|
||||
)}
|
||||
</Label>
|
||||
{field.description && (
|
||||
<p className='text-slate-500 text-xs'>{field.description}</p>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
{field.description}
|
||||
</p>
|
||||
)}
|
||||
{renderFieldInput(field)}
|
||||
{formErrors[field.name] && (
|
||||
<p className='text-red-600 text-xs'>{formErrors[field.name]}</p>
|
||||
<p className='text-[12px] text-[var(--text-error)]'>
|
||||
{formErrors[field.name]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-sm'>Resume Input (JSON)</CardTitle>
|
||||
<CardDescription>
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)]'>
|
||||
<div className='border-[var(--border)] border-b bg-[var(--surface-3)] px-[16px] py-[10px]'>
|
||||
<h3 className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Resume Input (JSON)
|
||||
</h3>
|
||||
<p className='mt-[2px] text-[12px] text-[var(--text-muted)]'>
|
||||
Provide optional JSON input to pass to the resumed execution
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</p>
|
||||
</div>
|
||||
<div className='p-[16px]'>
|
||||
<Textarea
|
||||
id='resume-input-textarea'
|
||||
value={resumeInput}
|
||||
@@ -1407,29 +1356,19 @@ export default function ResumeExecutionPage({
|
||||
}
|
||||
}}
|
||||
placeholder='{ "example": "value" }'
|
||||
className='min-h-[200px] font-mono text-sm'
|
||||
className='min-h-[200px] font-mono text-[13px]'
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error/Success Messages */}
|
||||
{error && (
|
||||
<Card className='border-red-200 bg-red-50'>
|
||||
<CardContent className='flex items-start gap-3 p-4'>
|
||||
<XCircle className='mt-0.5 h-5 w-5 flex-shrink-0 text-red-600' />
|
||||
<div className='text-red-700 text-sm'>{error}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
|
||||
|
||||
{message && (
|
||||
<Card className='border-emerald-200 bg-emerald-50'>
|
||||
<CardContent className='flex items-start gap-3 p-4'>
|
||||
<CheckCircle2 className='mt-0.5 h-5 w-5 flex-shrink-0 text-emerald-600' />
|
||||
<div className='text-emerald-700 text-sm'>{message}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='rounded-[6px] border border-[var(--text-success)]/30 bg-[var(--text-success)]/10 p-[16px]'>
|
||||
<p className='text-[13px] text-[var(--text-success)]'>{message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
@@ -1438,10 +1377,8 @@ export default function ResumeExecutionPage({
|
||||
type='button'
|
||||
onClick={handleResume}
|
||||
disabled={resumeDisabled}
|
||||
className='gap-2'
|
||||
size='lg'
|
||||
variant='tertiary'
|
||||
>
|
||||
<Play className='h-4 w-4' />
|
||||
{loadingAction ? 'Resuming...' : 'Resume Execution'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1451,11 +1388,11 @@ export default function ResumeExecutionPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-t bg-white py-4 text-center text-slate-600 text-sm'>
|
||||
<div className='border-[var(--border)] border-t bg-[var(--surface-1)] py-[16px] text-center text-[13px] text-[var(--text-secondary)]'>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='text-blue-600 underline-offset-4 transition hover:underline'
|
||||
className='text-[var(--brand-primary)] underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
|
||||
interface ChunkContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: { x: number; y: number }
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
onClose: () => void
|
||||
/**
|
||||
* Chunk-specific actions (shown when right-clicking on a chunk)
|
||||
*/
|
||||
onOpenInNewTab?: () => void
|
||||
onEdit?: () => void
|
||||
onCopyContent?: () => void
|
||||
onToggleEnabled?: () => void
|
||||
onDelete?: () => void
|
||||
/**
|
||||
* Empty space action (shown when right-clicking on empty space)
|
||||
*/
|
||||
onAddChunk?: () => void
|
||||
/**
|
||||
* Whether the chunk is currently enabled
|
||||
*/
|
||||
isChunkEnabled?: boolean
|
||||
/**
|
||||
* Whether a chunk is selected (vs empty space)
|
||||
*/
|
||||
hasChunk: boolean
|
||||
/**
|
||||
* Whether toggle enabled is disabled
|
||||
*/
|
||||
disableToggleEnabled?: boolean
|
||||
/**
|
||||
* Whether delete is disabled
|
||||
*/
|
||||
disableDelete?: boolean
|
||||
/**
|
||||
* Whether add chunk is disabled
|
||||
*/
|
||||
disableAddChunk?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu for chunks table.
|
||||
* Shows chunk actions when right-clicking a row, or "Create chunk" when right-clicking empty space.
|
||||
*/
|
||||
export function ChunkContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
onOpenInNewTab,
|
||||
onEdit,
|
||||
onCopyContent,
|
||||
onToggleEnabled,
|
||||
onDelete,
|
||||
onAddChunk,
|
||||
isChunkEnabled = true,
|
||||
hasChunk,
|
||||
disableToggleEnabled = false,
|
||||
disableDelete = false,
|
||||
disableAddChunk = false,
|
||||
}: ChunkContextMenuProps) {
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{hasChunk ? (
|
||||
<>
|
||||
{onOpenInNewTab && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onOpenInNewTab()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onEdit && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onEdit()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onCopyContent && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onCopyContent()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Copy content
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onToggleEnabled && (
|
||||
<PopoverItem
|
||||
disabled={disableToggleEnabled}
|
||||
onClick={() => {
|
||||
onToggleEnabled()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
{isChunkEnabled ? 'Disable' : 'Enable'}
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onDelete && (
|
||||
<PopoverItem
|
||||
disabled={disableDelete}
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</PopoverItem>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
onAddChunk && (
|
||||
<PopoverItem
|
||||
disabled={disableAddChunk}
|
||||
onClick={() => {
|
||||
onAddChunk()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Create chunk
|
||||
</PopoverItem>
|
||||
)
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ChunkContextMenu } from './chunk-context-menu'
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
|
||||
import type { DocumentData } from '@/lib/knowledge/types'
|
||||
import { knowledgeKeys } from '@/hooks/queries/knowledge'
|
||||
|
||||
const logger = createLogger('CreateChunkModal')
|
||||
|
||||
@@ -22,7 +23,6 @@ interface CreateChunkModalProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
document: DocumentData | null
|
||||
knowledgeBaseId: string
|
||||
onChunkCreated?: (chunk: ChunkData) => void
|
||||
}
|
||||
|
||||
export function CreateChunkModal({
|
||||
@@ -30,8 +30,8 @@ export function CreateChunkModal({
|
||||
onOpenChange,
|
||||
document,
|
||||
knowledgeBaseId,
|
||||
onChunkCreated,
|
||||
}: CreateChunkModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [content, setContent] = useState('')
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -77,9 +77,9 @@ export function CreateChunkModal({
|
||||
if (result.success && result.data) {
|
||||
logger.info('Chunk created successfully:', result.data.id)
|
||||
|
||||
if (onChunkCreated) {
|
||||
onChunkCreated(result.data)
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.detail(knowledgeBaseId),
|
||||
})
|
||||
|
||||
onClose()
|
||||
} else {
|
||||
@@ -96,7 +96,6 @@ export function CreateChunkModal({
|
||||
|
||||
const onClose = () => {
|
||||
onOpenChange(false)
|
||||
// Reset form state when modal closes
|
||||
setContent('')
|
||||
setError(null)
|
||||
setShowUnsavedChangesAlert(false)
|
||||
@@ -126,13 +125,7 @@ export function CreateChunkModal({
|
||||
<form>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-[var(--text-error)]' />
|
||||
<p className='text-[var(--text-error)] text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
|
||||
|
||||
{/* Content Input Section */}
|
||||
<Label htmlFor='content'>Chunk</Label>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import type { ChunkData } from '@/stores/knowledge/store'
|
||||
import type { ChunkData } from '@/lib/knowledge/types'
|
||||
import { knowledgeKeys } from '@/hooks/queries/knowledge'
|
||||
|
||||
const logger = createLogger('DeleteChunkModal')
|
||||
|
||||
@@ -13,7 +15,6 @@ interface DeleteChunkModalProps {
|
||||
documentId: string
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onChunkDeleted?: () => void
|
||||
}
|
||||
|
||||
export function DeleteChunkModal({
|
||||
@@ -22,8 +23,8 @@ export function DeleteChunkModal({
|
||||
documentId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onChunkDeleted,
|
||||
}: DeleteChunkModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const handleDeleteChunk = async () => {
|
||||
@@ -47,16 +48,17 @@ export function DeleteChunkModal({
|
||||
|
||||
if (result.success) {
|
||||
logger.info('Chunk deleted successfully:', chunk.id)
|
||||
if (onChunkDeleted) {
|
||||
onChunkDeleted()
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.detail(knowledgeBaseId),
|
||||
})
|
||||
|
||||
onClose()
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to delete chunk')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error deleting chunk:', err)
|
||||
// You might want to show an error state here
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ import {
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { ALL_TAG_SLOTS, type AllTagSlot, MAX_TAG_SLOTS } from '@/lib/knowledge/constants'
|
||||
import type { DocumentTag } from '@/lib/knowledge/tags/types'
|
||||
import type { DocumentData } from '@/lib/knowledge/types'
|
||||
import {
|
||||
type TagDefinition,
|
||||
useKnowledgeBaseTagDefinitions,
|
||||
} from '@/hooks/use-knowledge-base-tag-definitions'
|
||||
import { useNextAvailableSlot } from '@/hooks/use-next-available-slot'
|
||||
import { type TagDefinitionInput, useTagDefinitions } from '@/hooks/use-tag-definitions'
|
||||
import { type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
|
||||
|
||||
const logger = createLogger('DocumentTagsModal')
|
||||
|
||||
@@ -93,8 +93,6 @@ export function DocumentTagsModal({
|
||||
documentData,
|
||||
onDocumentUpdate,
|
||||
}: DocumentTagsModalProps) {
|
||||
const { updateDocument: updateDocumentInStore } = useKnowledgeStore()
|
||||
|
||||
const documentTagHook = useTagDefinitions(knowledgeBaseId, documentId)
|
||||
const kbTagHook = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
const { getNextAvailableSlot: getServerNextSlot } = useNextAvailableSlot(knowledgeBaseId)
|
||||
@@ -171,23 +169,14 @@ export function DocumentTagsModal({
|
||||
throw new Error('Failed to update document tags')
|
||||
}
|
||||
|
||||
updateDocumentInStore(knowledgeBaseId, documentId, tagData as Record<string, string>)
|
||||
onDocumentUpdate?.(tagData as Record<string, string>)
|
||||
|
||||
await fetchTagDefinitions()
|
||||
} catch (error) {
|
||||
logger.error('Error updating document tags:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[
|
||||
documentData,
|
||||
knowledgeBaseId,
|
||||
documentId,
|
||||
updateDocumentInStore,
|
||||
fetchTagDefinitions,
|
||||
onDocumentUpdate,
|
||||
]
|
||||
[documentData, knowledgeBaseId, documentId, fetchTagDefinitions, onDocumentUpdate]
|
||||
)
|
||||
|
||||
const handleRemoveTag = async (index: number) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
@@ -12,11 +12,14 @@ import {
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Switch,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import type { ChunkData, DocumentData } from '@/lib/knowledge/types'
|
||||
import { getAccurateTokenCount, getTokenStrings } from '@/lib/tokenization/estimators'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
|
||||
import { knowledgeKeys } from '@/hooks/queries/knowledge'
|
||||
|
||||
const logger = createLogger('EditChunkModal')
|
||||
|
||||
@@ -26,13 +29,12 @@ interface EditChunkModalProps {
|
||||
knowledgeBaseId: string
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onChunkUpdate?: (updatedChunk: ChunkData) => void
|
||||
// New props for navigation
|
||||
allChunks?: ChunkData[]
|
||||
currentPage?: number
|
||||
totalPages?: number
|
||||
onNavigateToChunk?: (chunk: ChunkData) => void
|
||||
onNavigateToPage?: (page: number, selectChunk: 'first' | 'last') => Promise<void>
|
||||
maxChunkSize?: number
|
||||
}
|
||||
|
||||
export function EditChunkModal({
|
||||
@@ -41,13 +43,14 @@ export function EditChunkModal({
|
||||
knowledgeBaseId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onChunkUpdate,
|
||||
allChunks = [],
|
||||
currentPage = 1,
|
||||
totalPages = 1,
|
||||
onNavigateToChunk,
|
||||
onNavigateToPage,
|
||||
maxChunkSize,
|
||||
}: EditChunkModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const [editedContent, setEditedContent] = useState(chunk?.content || '')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@@ -55,9 +58,39 @@ export function EditChunkModal({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
|
||||
const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null)
|
||||
const [tokenizerOn, setTokenizerOn] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const hasUnsavedChanges = editedContent !== (chunk?.content || '')
|
||||
|
||||
const tokenStrings = useMemo(() => {
|
||||
if (!tokenizerOn || !editedContent) return []
|
||||
return getTokenStrings(editedContent)
|
||||
}, [editedContent, tokenizerOn])
|
||||
|
||||
const tokenCount = useMemo(() => {
|
||||
if (!editedContent) return 0
|
||||
if (tokenizerOn) return tokenStrings.length
|
||||
return getAccurateTokenCount(editedContent)
|
||||
}, [editedContent, tokenizerOn, tokenStrings])
|
||||
|
||||
const TOKEN_BG_COLORS = [
|
||||
'rgba(239, 68, 68, 0.55)', // Red
|
||||
'rgba(249, 115, 22, 0.55)', // Orange
|
||||
'rgba(234, 179, 8, 0.55)', // Yellow
|
||||
'rgba(132, 204, 22, 0.55)', // Lime
|
||||
'rgba(34, 197, 94, 0.55)', // Green
|
||||
'rgba(20, 184, 166, 0.55)', // Teal
|
||||
'rgba(6, 182, 212, 0.55)', // Cyan
|
||||
'rgba(59, 130, 246, 0.55)', // Blue
|
||||
'rgba(139, 92, 246, 0.55)', // Violet
|
||||
'rgba(217, 70, 239, 0.55)', // Fuchsia
|
||||
]
|
||||
|
||||
const getTokenBgColor = (index: number): string => {
|
||||
return TOKEN_BG_COLORS[index % TOKEN_BG_COLORS.length]
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (chunk?.content) {
|
||||
setEditedContent(chunk.content)
|
||||
@@ -96,8 +129,10 @@ export function EditChunkModal({
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && onChunkUpdate) {
|
||||
onChunkUpdate(result.data)
|
||||
if (result.success) {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.detail(knowledgeBaseId),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error updating chunk:', err)
|
||||
@@ -125,7 +160,6 @@ export function EditChunkModal({
|
||||
const nextChunk = allChunks[currentChunkIndex + 1]
|
||||
onNavigateToChunk?.(nextChunk)
|
||||
} else if (currentPage < totalPages) {
|
||||
// Load next page and navigate to first chunk
|
||||
await onNavigateToPage?.(currentPage + 1, 'first')
|
||||
}
|
||||
}
|
||||
@@ -173,12 +207,9 @@ export function EditChunkModal({
|
||||
<>
|
||||
<Modal open={isOpen} onOpenChange={handleCloseAttempt}>
|
||||
<ModalContent size='lg'>
|
||||
<div className='flex items-center justify-between px-[16px] py-[10px]'>
|
||||
<DialogPrimitive.Title className='font-medium text-[16px] text-[var(--text-primary)]'>
|
||||
Edit Chunk #{chunk.chunkIndex}
|
||||
</DialogPrimitive.Title>
|
||||
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
<ModalHeader>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span>Edit Chunk #{chunk.chunkIndex}</span>
|
||||
{/* Navigation Controls */}
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Tooltip.Root>
|
||||
@@ -225,42 +256,60 @@ export function EditChunkModal({
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[16px] w-[16px] p-0'
|
||||
onClick={handleCloseAttempt}
|
||||
>
|
||||
<X className='h-[16px] w-[16px]' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<form>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-[var(--text-error)]' />
|
||||
<p className='text-[var(--text-error)] text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
|
||||
|
||||
{/* Content Input Section */}
|
||||
<Label htmlFor='content'>Chunk</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder={
|
||||
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
|
||||
}
|
||||
rows={20}
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
{tokenizerOn ? (
|
||||
/* Tokenizer view - matches Textarea styling exactly (transparent border for spacing) */
|
||||
<div
|
||||
className='h-[418px] overflow-y-auto whitespace-pre-wrap break-words rounded-[4px] border border-transparent bg-[var(--surface-5)] px-[8px] py-[8px] font-medium font-sans text-[var(--text-primary)] text-sm'
|
||||
style={{ minHeight: '418px' }}
|
||||
>
|
||||
{tokenStrings.map((token, index) => (
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: getTokenBgColor(index),
|
||||
}}
|
||||
>
|
||||
{token}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Edit view - regular textarea */
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
id='content'
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder={
|
||||
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
|
||||
}
|
||||
rows={20}
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tokenizer Section */}
|
||||
<div className='flex items-center justify-between pt-[12px]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='text-[12px] text-[var(--text-secondary)]'>Tokenizer</span>
|
||||
<Switch checked={tokenizerOn} onCheckedChange={setTokenizerOn} />
|
||||
</div>
|
||||
<span className='text-[12px] text-[var(--text-secondary)]'>
|
||||
{tokenCount.toLocaleString()}
|
||||
{maxChunkSize !== undefined && `/${maxChunkSize.toLocaleString()}`} tokens
|
||||
</span>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { ChunkContextMenu } from './chunk-context-menu'
|
||||
export { CreateChunkModal } from './create-chunk-modal/create-chunk-modal'
|
||||
export { DeleteChunkModal } from './delete-chunk-modal/delete-chunk-modal'
|
||||
export { DocumentTagsModal } from './document-tags-modal/document-tags-modal'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { startTransition, useCallback, useEffect, useState } from 'react'
|
||||
import { startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
@@ -17,27 +17,27 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Tooltip,
|
||||
Trash,
|
||||
} from '@/components/emcn'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
Tooltip,
|
||||
Trash,
|
||||
} from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import type { ChunkData } from '@/lib/knowledge/types'
|
||||
import {
|
||||
ChunkContextMenu,
|
||||
CreateChunkModal,
|
||||
DeleteChunkModal,
|
||||
DocumentTagsModal,
|
||||
@@ -45,9 +45,9 @@ import {
|
||||
} from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components'
|
||||
import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import { knowledgeKeys } from '@/hooks/queries/knowledge'
|
||||
import { useDocumentChunks } from '@/hooks/use-knowledge'
|
||||
import { type ChunkData, type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
|
||||
import { useDocument, useDocumentChunks, useKnowledgeBase } from '@/hooks/use-knowledge'
|
||||
|
||||
const logger = createLogger('Document')
|
||||
|
||||
@@ -262,12 +262,6 @@ export function Document({
|
||||
knowledgeBaseName,
|
||||
documentName,
|
||||
}: DocumentProps) {
|
||||
const {
|
||||
getCachedKnowledgeBase,
|
||||
getCachedDocuments,
|
||||
updateDocument: updateDocumentInStore,
|
||||
removeDocument,
|
||||
} = useKnowledgeStore()
|
||||
const queryClient = useQueryClient()
|
||||
const { workspaceId } = useParams()
|
||||
const router = useRouter()
|
||||
@@ -275,22 +269,19 @@ export function Document({
|
||||
const currentPageFromURL = Number.parseInt(searchParams.get('page') || '1', 10)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
/**
|
||||
* Get cached document synchronously for immediate render
|
||||
*/
|
||||
const getInitialCachedDocument = useCallback(() => {
|
||||
const cachedDocuments = getCachedDocuments(knowledgeBaseId)
|
||||
return cachedDocuments?.documents?.find((d) => d.id === documentId) || null
|
||||
}, [getCachedDocuments, knowledgeBaseId, documentId])
|
||||
const { knowledgeBase } = useKnowledgeBase(knowledgeBaseId)
|
||||
const {
|
||||
document: documentData,
|
||||
isLoading: isLoadingDocument,
|
||||
error: documentError,
|
||||
} = useDocument(knowledgeBaseId, documentId)
|
||||
|
||||
const [showTagsModal, setShowTagsModal] = useState(false)
|
||||
|
||||
// Search state management
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('')
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
|
||||
// Load initial chunks (no search) for immediate display
|
||||
const {
|
||||
chunks: initialChunks,
|
||||
currentPage: initialPage,
|
||||
@@ -301,16 +292,13 @@ export function Document({
|
||||
error: initialError,
|
||||
refreshChunks: initialRefreshChunks,
|
||||
updateChunk: initialUpdateChunk,
|
||||
} = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL, '', {
|
||||
enableClientSearch: false,
|
||||
})
|
||||
isFetching: isFetchingChunks,
|
||||
} = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL)
|
||||
|
||||
// Search results state
|
||||
const [searchResults, setSearchResults] = useState<ChunkData[]>([])
|
||||
const [isLoadingSearch, setIsLoadingSearch] = useState(false)
|
||||
const [searchError, setSearchError] = useState<string | null>(null)
|
||||
|
||||
// Load all search results when query changes
|
||||
useEffect(() => {
|
||||
if (!debouncedSearchQuery.trim()) {
|
||||
setSearchResults([])
|
||||
@@ -328,7 +316,7 @@ export function Document({
|
||||
const allResults: ChunkData[] = []
|
||||
let hasMore = true
|
||||
let offset = 0
|
||||
const limit = 100 // Larger batches for search
|
||||
const limit = 100
|
||||
|
||||
while (hasMore && isMounted) {
|
||||
const response = await fetch(
|
||||
@@ -375,7 +363,6 @@ export function Document({
|
||||
const [selectedChunk, setSelectedChunk] = useState<ChunkData | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
// Debounce search query with 200ms delay for optimal UX
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
startTransition(() => {
|
||||
@@ -389,12 +376,7 @@ export function Document({
|
||||
}
|
||||
}, [searchQuery])
|
||||
|
||||
// Determine which data to show
|
||||
const showingSearch = isSearching && searchQuery.trim().length > 0 && searchResults.length > 0
|
||||
|
||||
// Removed unused allDisplayChunks variable
|
||||
|
||||
// Client-side pagination for search results
|
||||
const SEARCH_PAGE_SIZE = 50
|
||||
const maxSearchPages = Math.ceil(searchResults.length / SEARCH_PAGE_SIZE)
|
||||
const searchCurrentPage =
|
||||
@@ -416,7 +398,6 @@ export function Document({
|
||||
|
||||
const goToPage = useCallback(
|
||||
async (page: number) => {
|
||||
// Update URL first for both modes
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (page > 1) {
|
||||
params.set('page', page.toString())
|
||||
@@ -426,10 +407,8 @@ export function Document({
|
||||
window.history.replaceState(null, '', `?${params.toString()}`)
|
||||
|
||||
if (showingSearch) {
|
||||
// For search, URL update is sufficient (client-side pagination)
|
||||
return
|
||||
}
|
||||
// For normal view, also trigger server-side pagination
|
||||
return await initialGoToPage(page)
|
||||
},
|
||||
[showingSearch, initialGoToPage]
|
||||
@@ -450,69 +429,24 @@ export function Document({
|
||||
const refreshChunks = showingSearch ? async () => {} : initialRefreshChunks
|
||||
const updateChunk = showingSearch ? (id: string, updates: any) => {} : initialUpdateChunk
|
||||
|
||||
const initialCachedDoc = getInitialCachedDocument()
|
||||
const [documentData, setDocumentData] = useState<DocumentData | null>(initialCachedDoc)
|
||||
const [isLoadingDocument, setIsLoadingDocument] = useState(!initialCachedDoc)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [isCreateChunkModalOpen, setIsCreateChunkModalOpen] = useState(false)
|
||||
const [chunkToDelete, setChunkToDelete] = useState<ChunkData | null>(null)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const [isBulkOperating, setIsBulkOperating] = useState(false)
|
||||
const [showDeleteDocumentDialog, setShowDeleteDocumentDialog] = useState(false)
|
||||
const [isDeletingDocument, setIsDeletingDocument] = useState(false)
|
||||
const [contextMenuChunk, setContextMenuChunk] = useState<ChunkData | null>(null)
|
||||
|
||||
const combinedError = error || searchError || initialError
|
||||
const {
|
||||
isOpen: isContextMenuOpen,
|
||||
position: contextMenuPosition,
|
||||
menuRef,
|
||||
handleContextMenu: baseHandleContextMenu,
|
||||
closeMenu: closeContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
// URL updates are handled directly in goToPage function to prevent pagination conflicts
|
||||
const combinedError = documentError || searchError || initialError
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDocument = async () => {
|
||||
// Check for cached data first
|
||||
const cachedDocuments = getCachedDocuments(knowledgeBaseId)
|
||||
const cachedDoc = cachedDocuments?.documents?.find((d) => d.id === documentId)
|
||||
|
||||
if (cachedDoc) {
|
||||
setDocumentData(cachedDoc)
|
||||
setIsLoadingDocument(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Only show loading and fetch if we don't have cached data
|
||||
setIsLoadingDocument(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/documents/${documentId}`)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Document not found')
|
||||
}
|
||||
throw new Error(`Failed to fetch document: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
setDocumentData(result.data)
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to fetch document')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error fetching document:', err)
|
||||
setError(err instanceof Error ? err.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoadingDocument(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (knowledgeBaseId && documentId) {
|
||||
fetchDocument()
|
||||
}
|
||||
}, [knowledgeBaseId, documentId, getCachedDocuments])
|
||||
|
||||
const knowledgeBase = getCachedKnowledgeBase(knowledgeBaseId)
|
||||
const effectiveKnowledgeBaseName = knowledgeBase?.name || knowledgeBaseName || 'Knowledge Base'
|
||||
const effectiveDocumentName = documentData?.filename || documentName || 'Document'
|
||||
|
||||
@@ -575,8 +509,7 @@ export function Document({
|
||||
}
|
||||
}
|
||||
|
||||
const handleChunkDeleted = async () => {
|
||||
await refreshChunks()
|
||||
const handleCloseDeleteModal = () => {
|
||||
if (chunkToDelete) {
|
||||
setSelectedChunks((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
@@ -584,9 +517,6 @@ export function Document({
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseDeleteModal = () => {
|
||||
setIsDeleteModalOpen(false)
|
||||
setChunkToDelete(null)
|
||||
}
|
||||
@@ -611,11 +541,6 @@ export function Document({
|
||||
}
|
||||
}
|
||||
|
||||
const handleChunkCreated = async () => {
|
||||
// Refresh the chunks list to include the new chunk
|
||||
await refreshChunks()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deleting the document
|
||||
*/
|
||||
@@ -636,9 +561,6 @@ export function Document({
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
removeDocument(knowledgeBaseId, documentId)
|
||||
|
||||
// Invalidate React Query cache to ensure fresh data on KB page
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.detail(knowledgeBaseId),
|
||||
})
|
||||
@@ -653,7 +575,6 @@ export function Document({
|
||||
}
|
||||
}
|
||||
|
||||
// Shared utility function for bulk chunk operations
|
||||
const performBulkChunkOperation = async (
|
||||
operation: 'enable' | 'disable' | 'delete',
|
||||
chunks: ChunkData[]
|
||||
@@ -685,10 +606,8 @@ export function Document({
|
||||
|
||||
if (result.success) {
|
||||
if (operation === 'delete') {
|
||||
// Refresh chunks list to reflect deletions
|
||||
await refreshChunks()
|
||||
} else {
|
||||
// Update successful chunks in the store for enable/disable operations
|
||||
result.data.results.forEach((opResult: any) => {
|
||||
if (opResult.operation === operation) {
|
||||
opResult.chunkIds.forEach((chunkId: string) => {
|
||||
@@ -701,7 +620,6 @@ export function Document({
|
||||
logger.info(`Successfully ${operation}d ${result.data.successCount} chunks`)
|
||||
}
|
||||
|
||||
// Clear selection after successful operation
|
||||
setSelectedChunks(new Set())
|
||||
} catch (err) {
|
||||
logger.error(`Error ${operation}ing chunks:`, err)
|
||||
@@ -729,22 +647,60 @@ export function Document({
|
||||
await performBulkChunkOperation('delete', chunksToDelete)
|
||||
}
|
||||
|
||||
// Calculate bulk operation counts
|
||||
const selectedChunksList = displayChunks.filter((chunk) => selectedChunks.has(chunk.id))
|
||||
const enabledCount = selectedChunksList.filter((chunk) => chunk.enabled).length
|
||||
const disabledCount = selectedChunksList.filter((chunk) => !chunk.enabled).length
|
||||
|
||||
const isAllSelected = displayChunks.length > 0 && selectedChunks.size === displayChunks.length
|
||||
|
||||
const handleDocumentTagsUpdate = useCallback(
|
||||
(tagData: Record<string, string>) => {
|
||||
updateDocumentInStore(knowledgeBaseId, documentId, tagData)
|
||||
setDocumentData((prev) => (prev ? { ...prev, ...tagData } : null))
|
||||
/**
|
||||
* Handle right-click on a chunk row
|
||||
*/
|
||||
const handleChunkContextMenu = useCallback(
|
||||
(e: React.MouseEvent, chunk: ChunkData) => {
|
||||
setContextMenuChunk(chunk)
|
||||
baseHandleContextMenu(e)
|
||||
},
|
||||
[knowledgeBaseId, documentId, updateDocumentInStore]
|
||||
[baseHandleContextMenu]
|
||||
)
|
||||
|
||||
if (isLoadingDocument) {
|
||||
/**
|
||||
* Handle right-click on empty space (table container)
|
||||
*/
|
||||
const handleEmptyContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setContextMenuChunk(null)
|
||||
baseHandleContextMenu(e)
|
||||
},
|
||||
[baseHandleContextMenu]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle context menu close
|
||||
*/
|
||||
const handleContextMenuClose = useCallback(() => {
|
||||
closeContextMenu()
|
||||
setContextMenuChunk(null)
|
||||
}, [closeContextMenu])
|
||||
|
||||
const handleDocumentTagsUpdate = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.document(knowledgeBaseId, documentId),
|
||||
})
|
||||
}, [knowledgeBaseId, documentId, queryClient])
|
||||
|
||||
const prevDocumentIdRef = useRef<string>(documentId)
|
||||
const isNavigatingToNewDoc = prevDocumentIdRef.current !== documentId
|
||||
|
||||
useEffect(() => {
|
||||
if (documentData && documentData.id === documentId) {
|
||||
prevDocumentIdRef.current = documentId
|
||||
}
|
||||
}, [documentData, documentId])
|
||||
|
||||
const isFetchingNewDoc = isNavigatingToNewDoc && isFetchingChunks
|
||||
|
||||
if (isLoadingDocument || isFetchingNewDoc) {
|
||||
return (
|
||||
<DocumentLoading
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
@@ -894,7 +850,10 @@ export function Document({
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
<div className='mt-[12px] flex flex-1 flex-col overflow-hidden'>
|
||||
<div
|
||||
className='mt-[12px] flex flex-1 flex-col overflow-hidden'
|
||||
onContextMenu={handleEmptyContextMenu}
|
||||
>
|
||||
{displayChunks.length === 0 && documentData?.processingStatus === 'completed' ? (
|
||||
<div className='mt-[10px] flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
|
||||
<div className='text-center'>
|
||||
@@ -920,6 +879,7 @@ export function Document({
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
disabled={
|
||||
@@ -927,7 +887,6 @@ export function Document({
|
||||
!userPermissions.canEdit
|
||||
}
|
||||
aria-label='Select all chunks'
|
||||
className='h-[14px] w-[14px] border-[var(--border-2)] focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-[12px] [&>*]:w-[12px]'
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
@@ -992,6 +951,7 @@ export function Document({
|
||||
key={chunk.id}
|
||||
className='cursor-pointer hover:bg-[var(--surface-2)]'
|
||||
onClick={() => handleChunkClick(chunk)}
|
||||
onContextMenu={(e) => handleChunkContextMenu(e, chunk)}
|
||||
>
|
||||
<TableCell
|
||||
className='w-[52px] py-[8px]'
|
||||
@@ -999,13 +959,13 @@ export function Document({
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={selectedChunks.has(chunk.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleSelectChunk(chunk.id, checked as boolean)
|
||||
}
|
||||
disabled={!userPermissions.canEdit}
|
||||
aria-label={`Select chunk ${chunk.chunkIndex}`}
|
||||
className='h-[14px] w-[14px] border-[var(--border-2)] focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-[12px] [&>*]:w-[12px]'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1154,16 +1114,13 @@ export function Document({
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onChunkUpdate={(updatedChunk: ChunkData) => {
|
||||
updateChunk(updatedChunk.id, updatedChunk)
|
||||
setSelectedChunk(updatedChunk)
|
||||
}}
|
||||
allChunks={displayChunks}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onNavigateToChunk={(chunk: ChunkData) => {
|
||||
setSelectedChunk(chunk)
|
||||
}}
|
||||
maxChunkSize={knowledgeBase?.chunkingConfig?.maxSize}
|
||||
onNavigateToPage={async (page: number, selectChunk: 'first' | 'last') => {
|
||||
await goToPage(page)
|
||||
|
||||
@@ -1175,7 +1132,6 @@ export function Document({
|
||||
setSelectedChunk(displayChunks[displayChunks.length - 1])
|
||||
}
|
||||
} else {
|
||||
// Retry after a short delay if chunks aren't loaded yet
|
||||
setTimeout(checkAndSelectChunk, 100)
|
||||
}
|
||||
}
|
||||
@@ -1190,7 +1146,6 @@ export function Document({
|
||||
onOpenChange={setIsCreateChunkModalOpen}
|
||||
document={documentData}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
onChunkCreated={handleChunkCreated}
|
||||
/>
|
||||
|
||||
{/* Delete Chunk Modal */}
|
||||
@@ -1200,7 +1155,6 @@ export function Document({
|
||||
documentId={documentId}
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={handleCloseDeleteModal}
|
||||
onChunkDeleted={handleChunkDeleted}
|
||||
/>
|
||||
|
||||
{/* Bulk Action Bar */}
|
||||
@@ -1244,6 +1198,56 @@ export function Document({
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<ChunkContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
position={contextMenuPosition}
|
||||
menuRef={menuRef}
|
||||
onClose={handleContextMenuClose}
|
||||
hasChunk={contextMenuChunk !== null}
|
||||
isChunkEnabled={contextMenuChunk?.enabled ?? true}
|
||||
onOpenInNewTab={
|
||||
contextMenuChunk
|
||||
? () => {
|
||||
const url = `/workspace/${workspaceId}/knowledge/${knowledgeBaseId}/${documentId}?chunk=${contextMenuChunk.id}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onEdit={
|
||||
contextMenuChunk
|
||||
? () => {
|
||||
setSelectedChunk(contextMenuChunk)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onCopyContent={
|
||||
contextMenuChunk
|
||||
? () => {
|
||||
navigator.clipboard.writeText(contextMenuChunk.content)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onToggleEnabled={
|
||||
contextMenuChunk && userPermissions.canEdit
|
||||
? () => handleToggleEnabled(contextMenuChunk.id)
|
||||
: undefined
|
||||
}
|
||||
onDelete={
|
||||
contextMenuChunk && userPermissions.canEdit
|
||||
? () => handleDeleteChunk(contextMenuChunk.id)
|
||||
: undefined
|
||||
}
|
||||
onAddChunk={
|
||||
userPermissions.canEdit && documentData?.processingStatus !== 'failed'
|
||||
? () => setIsCreateChunkModalOpen(true)
|
||||
: undefined
|
||||
}
|
||||
disableToggleEnabled={!userPermissions.canEdit}
|
||||
disableDelete={!userPermissions.canEdit}
|
||||
disableAddChunk={!userPermissions.canEdit || documentData?.processingStatus === 'failed'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { format } from 'date-fns'
|
||||
import {
|
||||
@@ -21,35 +21,36 @@ import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Tooltip,
|
||||
Trash,
|
||||
} from '@/components/emcn'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
Tooltip,
|
||||
Trash,
|
||||
} from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||
import type { DocumentData } from '@/lib/knowledge/types'
|
||||
import {
|
||||
ActionBar,
|
||||
AddDocumentsModal,
|
||||
BaseTagsModal,
|
||||
DocumentContextMenu,
|
||||
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
|
||||
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import {
|
||||
useKnowledgeBase,
|
||||
useKnowledgeBaseDocuments,
|
||||
@@ -59,7 +60,6 @@ import {
|
||||
type TagDefinition,
|
||||
useKnowledgeBaseTagDefinitions,
|
||||
} from '@/hooks/use-knowledge-base-tag-definitions'
|
||||
import type { DocumentData } from '@/stores/knowledge/store'
|
||||
|
||||
const logger = createLogger('KnowledgeBase')
|
||||
|
||||
@@ -431,6 +431,15 @@ export function KnowledgeBase({
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [sortBy, setSortBy] = useState<DocumentSortField>('uploadedAt')
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||
const [contextMenuDocument, setContextMenuDocument] = useState<DocumentData | null>(null)
|
||||
|
||||
const {
|
||||
isOpen: isContextMenuOpen,
|
||||
position: contextMenuPosition,
|
||||
menuRef,
|
||||
handleContextMenu: baseHandleContextMenu,
|
||||
closeMenu: closeContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const {
|
||||
knowledgeBase,
|
||||
@@ -442,6 +451,8 @@ export function KnowledgeBase({
|
||||
documents,
|
||||
pagination,
|
||||
isLoading: isLoadingDocuments,
|
||||
isFetching: isFetchingDocuments,
|
||||
isPlaceholderData: isPlaceholderDocuments,
|
||||
error: documentsError,
|
||||
updateDocument,
|
||||
refreshDocuments,
|
||||
@@ -591,6 +602,10 @@ export function KnowledgeBase({
|
||||
const document = documents.find((doc) => doc.id === docId)
|
||||
if (!document) return
|
||||
|
||||
const newEnabled = !document.enabled
|
||||
|
||||
updateDocument(docId, { enabled: newEnabled })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge/${id}/documents/${docId}`, {
|
||||
method: 'PUT',
|
||||
@@ -598,7 +613,7 @@ export function KnowledgeBase({
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: !document.enabled,
|
||||
enabled: newEnabled,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -608,10 +623,11 @@ export function KnowledgeBase({
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
updateDocument(docId, { enabled: !document.enabled })
|
||||
if (!result.success) {
|
||||
updateDocument(docId, { enabled: !newEnabled })
|
||||
}
|
||||
} catch (err) {
|
||||
updateDocument(docId, { enabled: !newEnabled })
|
||||
logger.error('Error updating document:', err)
|
||||
}
|
||||
}
|
||||
@@ -834,7 +850,6 @@ export function KnowledgeBase({
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// Update successful documents in the store
|
||||
result.data.updatedDocuments.forEach((updatedDoc: { id: string; enabled: boolean }) => {
|
||||
updateDocument(updatedDoc.id, { enabled: updatedDoc.enabled })
|
||||
})
|
||||
@@ -842,7 +857,6 @@ export function KnowledgeBase({
|
||||
logger.info(`Successfully enabled ${result.data.successCount} documents`)
|
||||
}
|
||||
|
||||
// Clear selection after successful operation
|
||||
setSelectedDocuments(new Set())
|
||||
} catch (err) {
|
||||
logger.error('Error enabling documents:', err)
|
||||
@@ -952,7 +966,49 @@ export function KnowledgeBase({
|
||||
const enabledCount = selectedDocumentsList.filter((doc) => doc.enabled).length
|
||||
const disabledCount = selectedDocumentsList.filter((doc) => !doc.enabled).length
|
||||
|
||||
if ((isLoadingKnowledgeBase || isLoadingDocuments) && !knowledgeBase && documents.length === 0) {
|
||||
/**
|
||||
* Handle right-click on a document row
|
||||
*/
|
||||
const handleDocumentContextMenu = useCallback(
|
||||
(e: React.MouseEvent, doc: DocumentData) => {
|
||||
setContextMenuDocument(doc)
|
||||
baseHandleContextMenu(e)
|
||||
},
|
||||
[baseHandleContextMenu]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle right-click on empty space (table container)
|
||||
*/
|
||||
const handleEmptyContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setContextMenuDocument(null)
|
||||
baseHandleContextMenu(e)
|
||||
},
|
||||
[baseHandleContextMenu]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle context menu close
|
||||
*/
|
||||
const handleContextMenuClose = useCallback(() => {
|
||||
closeContextMenu()
|
||||
setContextMenuDocument(null)
|
||||
}, [closeContextMenu])
|
||||
|
||||
const prevKnowledgeBaseIdRef = useRef<string>(id)
|
||||
const isNavigatingToNewKB = prevKnowledgeBaseIdRef.current !== id
|
||||
|
||||
useEffect(() => {
|
||||
if (knowledgeBase && knowledgeBase.id === id) {
|
||||
prevKnowledgeBaseIdRef.current = id
|
||||
}
|
||||
}, [knowledgeBase, id])
|
||||
|
||||
const isInitialLoad = isLoadingKnowledgeBase && !knowledgeBase
|
||||
const isFetchingNewKB = isNavigatingToNewKB && isFetchingDocuments
|
||||
|
||||
if (isInitialLoad || isFetchingNewKB) {
|
||||
return <KnowledgeBaseLoading knowledgeBaseName={knowledgeBaseName} />
|
||||
}
|
||||
|
||||
@@ -1100,7 +1156,7 @@ export function KnowledgeBase({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-[12px] flex flex-1 flex-col'>
|
||||
<div className='mt-[12px] flex flex-1 flex-col' onContextMenu={handleEmptyContextMenu}>
|
||||
{isLoadingDocuments && documents.length === 0 ? (
|
||||
<DocumentTableSkeleton rowCount={5} />
|
||||
) : documents.length === 0 ? (
|
||||
@@ -1125,11 +1181,11 @@ export function KnowledgeBase({
|
||||
<TableHead className='w-[28px] py-[8px] pr-0 pl-0'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
disabled={!userPermissions.canEdit}
|
||||
aria-label='Select all documents'
|
||||
className='h-[14px] w-[14px] border-[var(--border-2)] focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-[12px] [&>*]:w-[12px]'
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
@@ -1162,6 +1218,7 @@ export function KnowledgeBase({
|
||||
handleDocumentClick(doc.id)
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => handleDocumentContextMenu(e, doc)}
|
||||
>
|
||||
<TableCell className='w-[28px] py-[8px] pr-0 pl-0'>
|
||||
<div className='flex items-center justify-center'>
|
||||
@@ -1170,10 +1227,10 @@ export function KnowledgeBase({
|
||||
onCheckedChange={(checked) =>
|
||||
handleSelectDocument(doc.id, checked as boolean)
|
||||
}
|
||||
size='sm'
|
||||
disabled={!userPermissions.canEdit}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Select ${doc.filename}`}
|
||||
className='h-[14px] w-[14px] border-[var(--border-2)] focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-[12px] [&>*]:w-[12px]'
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -1499,7 +1556,6 @@ export function KnowledgeBase({
|
||||
onOpenChange={setShowAddDocumentsModal}
|
||||
knowledgeBaseId={id}
|
||||
chunkingConfig={knowledgeBase?.chunkingConfig}
|
||||
onUploadComplete={refreshDocuments}
|
||||
/>
|
||||
|
||||
<ActionBar
|
||||
@@ -1511,6 +1567,67 @@ export function KnowledgeBase({
|
||||
disabledCount={disabledCount}
|
||||
isLoading={isBulkOperating}
|
||||
/>
|
||||
|
||||
<DocumentContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
position={contextMenuPosition}
|
||||
menuRef={menuRef}
|
||||
onClose={handleContextMenuClose}
|
||||
hasDocument={contextMenuDocument !== null}
|
||||
isDocumentEnabled={contextMenuDocument?.enabled ?? true}
|
||||
hasTags={
|
||||
contextMenuDocument
|
||||
? getDocumentTags(contextMenuDocument, tagDefinitions).length > 0
|
||||
: false
|
||||
}
|
||||
onOpenInNewTab={
|
||||
contextMenuDocument
|
||||
? () => {
|
||||
const urlParams = new URLSearchParams({
|
||||
kbName: knowledgeBaseName,
|
||||
docName: contextMenuDocument.filename || 'Document',
|
||||
})
|
||||
window.open(
|
||||
`/workspace/${workspaceId}/knowledge/${id}/${contextMenuDocument.id}?${urlParams.toString()}`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onToggleEnabled={
|
||||
contextMenuDocument && userPermissions.canEdit
|
||||
? () => handleToggleEnabled(contextMenuDocument.id)
|
||||
: undefined
|
||||
}
|
||||
onViewTags={
|
||||
contextMenuDocument
|
||||
? () => {
|
||||
const urlParams = new URLSearchParams({
|
||||
kbName: knowledgeBaseName,
|
||||
docName: contextMenuDocument.filename || 'Document',
|
||||
})
|
||||
router.push(
|
||||
`/workspace/${workspaceId}/knowledge/${id}/${contextMenuDocument.id}?${urlParams.toString()}`
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDelete={
|
||||
contextMenuDocument && userPermissions.canEdit
|
||||
? () => handleDeleteDocument(contextMenuDocument.id)
|
||||
: undefined
|
||||
}
|
||||
onAddDocument={userPermissions.canEdit ? handleAddDocuments : undefined}
|
||||
disableToggleEnabled={
|
||||
!userPermissions.canEdit ||
|
||||
contextMenuDocument?.processingStatus === 'processing' ||
|
||||
contextMenuDocument?.processingStatus === 'pending'
|
||||
}
|
||||
disableDelete={
|
||||
!userPermissions.canEdit || contextMenuDocument?.processingStatus === 'processing'
|
||||
}
|
||||
disableAddDocument={!userPermissions.canEdit}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ interface AddDocumentsModalProps {
|
||||
minSize: number
|
||||
overlap: number
|
||||
}
|
||||
onUploadComplete?: () => void
|
||||
}
|
||||
|
||||
export function AddDocumentsModal({
|
||||
@@ -41,7 +40,6 @@ export function AddDocumentsModal({
|
||||
onOpenChange,
|
||||
knowledgeBaseId,
|
||||
chunkingConfig,
|
||||
onUploadComplete,
|
||||
}: AddDocumentsModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
@@ -54,11 +52,6 @@ export function AddDocumentsModal({
|
||||
|
||||
const { isUploading, uploadProgress, uploadFiles, uploadError, clearError } = useKnowledgeUpload({
|
||||
workspaceId,
|
||||
onUploadComplete: () => {
|
||||
logger.info(`Successfully uploaded ${files.length} files`)
|
||||
onUploadComplete?.()
|
||||
handleClose()
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -219,6 +212,8 @@ export function AddDocumentsModal({
|
||||
chunkOverlap: chunkingConfig?.overlap || 200,
|
||||
recipe: 'default',
|
||||
})
|
||||
logger.info(`Successfully uploaded ${files.length} files`)
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
logger.error('Error uploading files:', error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
|
||||
interface DocumentContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: { x: number; y: number }
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
onClose: () => void
|
||||
/**
|
||||
* Document-specific actions (shown when right-clicking on a document)
|
||||
*/
|
||||
onOpenInNewTab?: () => void
|
||||
onToggleEnabled?: () => void
|
||||
onViewTags?: () => void
|
||||
onDelete?: () => void
|
||||
/**
|
||||
* Empty space action (shown when right-clicking on empty space)
|
||||
*/
|
||||
onAddDocument?: () => void
|
||||
/**
|
||||
* Whether the document is currently enabled
|
||||
*/
|
||||
isDocumentEnabled?: boolean
|
||||
/**
|
||||
* Whether a document is selected (vs empty space)
|
||||
*/
|
||||
hasDocument: boolean
|
||||
/**
|
||||
* Whether the document has tags to view
|
||||
*/
|
||||
hasTags?: boolean
|
||||
/**
|
||||
* Whether toggle enabled is disabled
|
||||
*/
|
||||
disableToggleEnabled?: boolean
|
||||
/**
|
||||
* Whether delete is disabled
|
||||
*/
|
||||
disableDelete?: boolean
|
||||
/**
|
||||
* Whether add document is disabled
|
||||
*/
|
||||
disableAddDocument?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu for documents table.
|
||||
* Shows document actions when right-clicking a row, or "Add Document" when right-clicking empty space.
|
||||
*/
|
||||
export function DocumentContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
onOpenInNewTab,
|
||||
onToggleEnabled,
|
||||
onViewTags,
|
||||
onDelete,
|
||||
onAddDocument,
|
||||
isDocumentEnabled = true,
|
||||
hasDocument,
|
||||
hasTags = false,
|
||||
disableToggleEnabled = false,
|
||||
disableDelete = false,
|
||||
disableAddDocument = false,
|
||||
}: DocumentContextMenuProps) {
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{hasDocument ? (
|
||||
<>
|
||||
{onOpenInNewTab && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onOpenInNewTab()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
)}
|
||||
{hasTags && onViewTags && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onViewTags()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
View tags
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onToggleEnabled && (
|
||||
<PopoverItem
|
||||
disabled={disableToggleEnabled}
|
||||
onClick={() => {
|
||||
onToggleEnabled()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
{isDocumentEnabled ? 'Disable' : 'Enable'}
|
||||
</PopoverItem>
|
||||
)}
|
||||
{onDelete && (
|
||||
<PopoverItem
|
||||
disabled={disableDelete}
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</PopoverItem>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
onAddDocument && (
|
||||
<PopoverItem
|
||||
disabled={disableAddDocument}
|
||||
onClick={() => {
|
||||
onAddDocument()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Add document
|
||||
</PopoverItem>
|
||||
)
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DocumentContextMenu } from './document-context-menu'
|
||||
@@ -1,3 +1,4 @@
|
||||
export { ActionBar } from './action-bar/action-bar'
|
||||
export { AddDocumentsModal } from './add-documents-modal/add-documents-modal'
|
||||
export { BaseTagsModal } from './base-tags-modal/base-tags-modal'
|
||||
export { DocumentContextMenu } from './document-context-menu'
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn'
|
||||
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import { DeleteKnowledgeBaseModal } from '../delete-knowledge-base-modal/delete-knowledge-base-modal'
|
||||
import { EditKnowledgeBaseModal } from '../edit-knowledge-base-modal/edit-knowledge-base-modal'
|
||||
import { KnowledgeBaseContextMenu } from '../knowledge-base-context-menu/knowledge-base-context-menu'
|
||||
|
||||
interface BaseCardProps {
|
||||
id?: string
|
||||
@@ -11,6 +17,8 @@ interface BaseCardProps {
|
||||
description: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
onUpdate?: (id: string, name: string, description: string) => Promise<void>
|
||||
onDelete?: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,9 +117,32 @@ export function BaseCardSkeletonGrid({ count = 8 }: { count?: number }) {
|
||||
/**
|
||||
* Knowledge base card component displaying overview information
|
||||
*/
|
||||
export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCardProps) {
|
||||
export function BaseCard({
|
||||
id,
|
||||
title,
|
||||
docCount,
|
||||
description,
|
||||
updatedAt,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: BaseCardProps) {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const workspaceId = params?.workspaceId as string
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const {
|
||||
isOpen: isContextMenuOpen,
|
||||
position: contextMenuPosition,
|
||||
menuRef,
|
||||
handleContextMenu,
|
||||
closeMenu: closeContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
kbName: title,
|
||||
@@ -120,41 +151,156 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
|
||||
|
||||
const shortId = id ? `kb-${id.slice(0, 8)}` : ''
|
||||
|
||||
return (
|
||||
<Link href={href} prefetch={true} className='h-full'>
|
||||
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
|
||||
<div className='flex items-center justify-between gap-[8px]'>
|
||||
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
|
||||
{title}
|
||||
</h3>
|
||||
{shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>}
|
||||
</div>
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isContextMenuOpen) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
router.push(href)
|
||||
},
|
||||
[isContextMenuOpen, router, href]
|
||||
)
|
||||
|
||||
<div className='flex flex-1 flex-col gap-[8px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
|
||||
<DocumentAttachment className='h-[12px] w-[12px]' />
|
||||
{docCount} {docCount === 1 ? 'doc' : 'docs'}
|
||||
</span>
|
||||
{updatedAt && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
last updated: {formatRelativeTime(updatedAt)}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
router.push(href)
|
||||
}
|
||||
},
|
||||
[router, href]
|
||||
)
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
window.open(href, '_blank')
|
||||
}, [href])
|
||||
|
||||
const handleViewTags = useCallback(() => {
|
||||
setIsTagsModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setIsEditModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setIsDeleteModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!id || !onDelete) return
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await onDelete(id)
|
||||
setIsDeleteModalOpen(false)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}, [id, onDelete])
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (knowledgeBaseId: string, name: string, newDescription: string) => {
|
||||
if (!onUpdate) return
|
||||
await onUpdate(knowledgeBaseId, name, newDescription)
|
||||
},
|
||||
[onUpdate]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-full cursor-pointer'
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-kb-card
|
||||
>
|
||||
<div className='group flex h-full flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
|
||||
<div className='flex items-center justify-between gap-[8px]'>
|
||||
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
|
||||
{title}
|
||||
</h3>
|
||||
{shortId && (
|
||||
<Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='h-0 w-full border-[var(--divider)] border-t' />
|
||||
<div className='flex flex-1 flex-col gap-[8px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
|
||||
<DocumentAttachment className='h-[12px] w-[12px]' />
|
||||
{docCount} {docCount === 1 ? 'doc' : 'docs'}
|
||||
</span>
|
||||
{updatedAt && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
last updated: {formatRelativeTime(updatedAt)}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
|
||||
{description}
|
||||
</p>
|
||||
<div className='h-0 w-full border-[var(--divider)] border-t' />
|
||||
|
||||
<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<KnowledgeBaseContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
position={contextMenuPosition}
|
||||
menuRef={menuRef}
|
||||
onClose={closeContextMenu}
|
||||
onOpenInNewTab={handleOpenInNewTab}
|
||||
onViewTags={handleViewTags}
|
||||
onCopyId={id ? () => navigator.clipboard.writeText(id) : undefined}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
showOpenInNewTab={true}
|
||||
showViewTags={!!id}
|
||||
showEdit={!!onUpdate}
|
||||
showDelete={!!onDelete}
|
||||
disableEdit={!userPermissions.canEdit}
|
||||
disableDelete={!userPermissions.canEdit}
|
||||
/>
|
||||
|
||||
{id && onUpdate && (
|
||||
<EditKnowledgeBaseModal
|
||||
open={isEditModalOpen}
|
||||
onOpenChange={setIsEditModalOpen}
|
||||
knowledgeBaseId={id}
|
||||
initialName={title}
|
||||
initialDescription={description === 'No description provided' ? '' : description}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
{id && onDelete && (
|
||||
<DeleteKnowledgeBaseModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={handleConfirmDelete}
|
||||
isDeleting={isDeleting}
|
||||
knowledgeBaseName={title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{id && (
|
||||
<BaseTagsModal
|
||||
open={isTagsModalOpen}
|
||||
onOpenChange={setIsTagsModalOpen}
|
||||
knowledgeBaseId={id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2, RotateCcw, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useForm } from 'react-hook-form'
|
||||
@@ -22,7 +23,7 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
|
||||
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
|
||||
import type { KnowledgeBaseData } from '@/stores/knowledge/store'
|
||||
import { knowledgeKeys } from '@/hooks/queries/knowledge'
|
||||
|
||||
const logger = createLogger('CreateBaseModal')
|
||||
|
||||
@@ -33,7 +34,6 @@ interface FileWithPreview extends File {
|
||||
interface CreateBaseModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onKnowledgeBaseCreated?: (knowledgeBase: KnowledgeBaseData) => void
|
||||
}
|
||||
|
||||
const FormSchema = z
|
||||
@@ -79,13 +79,10 @@ interface SubmitStatus {
|
||||
message: string
|
||||
}
|
||||
|
||||
export function CreateBaseModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onKnowledgeBaseCreated,
|
||||
}: CreateBaseModalProps) {
|
||||
export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -100,9 +97,6 @@ export function CreateBaseModal({
|
||||
|
||||
const { uploadFiles, isUploading, uploadProgress, uploadError, clearError } = useKnowledgeUpload({
|
||||
workspaceId,
|
||||
onUploadComplete: (uploadedFiles) => {
|
||||
logger.info(`Successfully uploaded ${uploadedFiles.length} files`)
|
||||
},
|
||||
})
|
||||
|
||||
const handleClose = (open: boolean) => {
|
||||
@@ -300,13 +294,10 @@ export function CreateBaseModal({
|
||||
logger.info(`Successfully uploaded ${uploadedFiles.length} files`)
|
||||
logger.info(`Started processing ${uploadedFiles.length} documents in the background`)
|
||||
|
||||
newKnowledgeBase.docCount = uploadedFiles.length
|
||||
|
||||
if (onKnowledgeBaseCreated) {
|
||||
onKnowledgeBaseCreated(newKnowledgeBase)
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.list(workspaceId),
|
||||
})
|
||||
} catch (uploadError) {
|
||||
// If file upload fails completely, delete the knowledge base to avoid orphaned empty KB
|
||||
logger.error('File upload failed, deleting knowledge base:', uploadError)
|
||||
try {
|
||||
await fetch(`/api/knowledge/${newKnowledgeBase.id}`, {
|
||||
@@ -319,9 +310,9 @@ export function CreateBaseModal({
|
||||
throw uploadError
|
||||
}
|
||||
} else {
|
||||
if (onKnowledgeBaseCreated) {
|
||||
onKnowledgeBaseCreated(newKnowledgeBase)
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.list(workspaceId),
|
||||
})
|
||||
}
|
||||
|
||||
files.forEach((file) => URL.revokeObjectURL(file.preview))
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
|
||||
interface DeleteKnowledgeBaseModalProps {
|
||||
/**
|
||||
* Whether the modal is open
|
||||
*/
|
||||
isOpen: boolean
|
||||
/**
|
||||
* Callback when modal should close
|
||||
*/
|
||||
onClose: () => void
|
||||
/**
|
||||
* Callback when delete is confirmed
|
||||
*/
|
||||
onConfirm: () => void
|
||||
/**
|
||||
* Whether the delete operation is in progress
|
||||
*/
|
||||
isDeleting: boolean
|
||||
/**
|
||||
* Name of the knowledge base being deleted
|
||||
*/
|
||||
knowledgeBaseName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete confirmation modal for knowledge base items.
|
||||
* Displays a warning message and confirmation buttons.
|
||||
*/
|
||||
export function DeleteKnowledgeBaseModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
isDeleting,
|
||||
knowledgeBaseName,
|
||||
}: DeleteKnowledgeBaseModalProps) {
|
||||
return (
|
||||
<Modal open={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Knowledge Base</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
{knowledgeBaseName ? (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
|
||||
This will permanently remove all associated documents, chunks, and embeddings.
|
||||
</>
|
||||
) : (
|
||||
'Are you sure you want to delete this knowledge base? This will permanently remove all associated documents, chunks, and embeddings.'
|
||||
)}{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='active' onClick={onClose} disabled={isDeleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='destructive' onClick={onConfirm} disabled={isDeleting}>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||