Skip to content

Commit 7391cd1

Browse files
waleedlatif1claude
andcommitted
feat(jsm): add ProForma/JSM Forms discovery tools (#4078)
* feat(jsm): add ProForma/JSM Forms discovery tools Add three new tools for discovering and inspecting JSM Forms (ProForma) templates and their structure, enabling dynamic form-based workflows: - jsm_get_form_templates: List form templates in a project with request type bindings - jsm_get_form_structure: Get full form design (questions, layout, conditions, sections) - jsm_get_issue_forms: List forms attached to an issue with submission status All endpoints validated against the official Atlassian Forms REST API OpenAPI spec. Uses the Forms Cloud API base URL (jira/forms/cloud/{cloudId}) with X-ExperimentalApi header. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(jsm): add input validation and extract shared error parser - Add validateJiraIssueKey for projectIdOrKey in templates and structure routes - Add validateJiraCloudId for formId (UUID) in structure route - Extract parseJsmErrorMessage to shared utils.ts (was duplicated across 3 routes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(jsm): remove unused FORM_QUESTION_PROPERTIES constant Dead code — the get_form_structure tool passes the raw design object through as JSON, so this output constant had no consumers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 149ad9f commit 7391cd1

File tree

13 files changed

+1023
-1
lines changed

13 files changed

+1023
-1
lines changed

apps/docs/content/docs/en/tools/jira_service_management.mdx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,4 +678,84 @@ Get the fields required to create a request of a specific type in Jira Service M
678678
|`defaultValues` | json | Default values for the field |
679679
|`jiraSchema` | json | Jira field schema with type, system, custom, customId |
680680

681+
### `jsm_get_form_templates`
682+
683+
List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types
684+
685+
#### Input
686+
687+
| Parameter | Type | Required | Description |
688+
| --------- | ---- | -------- | ----------- |
689+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
690+
| `cloudId` | string | No | Jira Cloud ID for the instance |
691+
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |
692+
693+
#### Output
694+
695+
| Parameter | Type | Description |
696+
| --------- | ---- | ----------- |
697+
| `ts` | string | Timestamp of the operation |
698+
| `projectIdOrKey` | string | Project ID or key |
699+
| `templates` | array | List of forms in the project |
700+
|`id` | string | Form template ID \(UUID\) |
701+
|`name` | string | Form template name |
702+
|`updated` | string | Last updated timestamp \(ISO 8601\) |
703+
|`issueCreateIssueTypeIds` | json | Issue type IDs that auto-attach this form on issue create |
704+
|`issueCreateRequestTypeIds` | json | Request type IDs that auto-attach this form on issue create |
705+
|`portalRequestTypeIds` | json | Request type IDs that show this form on the customer portal |
706+
|`recommendedIssueRequestTypeIds` | json | Request type IDs that recommend this form |
707+
| `total` | number | Total number of forms |
708+
709+
### `jsm_get_form_structure`
710+
711+
Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions
712+
713+
#### Input
714+
715+
| Parameter | Type | Required | Description |
716+
| --------- | ---- | -------- | ----------- |
717+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
718+
| `cloudId` | string | No | Jira Cloud ID for the instance |
719+
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |
720+
| `formId` | string | Yes | Form ID \(UUID from Get Form Templates\) |
721+
722+
#### Output
723+
724+
| Parameter | Type | Description |
725+
| --------- | ---- | ----------- |
726+
| `ts` | string | Timestamp of the operation |
727+
| `projectIdOrKey` | string | Project ID or key |
728+
| `formId` | string | Form ID |
729+
| `design` | json | Full form design with questions \(field types, labels, choices, validation\), layout \(field ordering\), and conditions |
730+
| `updated` | string | Last updated timestamp |
731+
| `publish` | json | Publishing and request type configuration |
732+
733+
### `jsm_get_issue_forms`
734+
735+
List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)
736+
737+
#### Input
738+
739+
| Parameter | Type | Required | Description |
740+
| --------- | ---- | -------- | ----------- |
741+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
742+
| `cloudId` | string | No | Jira Cloud ID for the instance |
743+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123", "10001"\) |
744+
745+
#### Output
746+
747+
| Parameter | Type | Description |
748+
| --------- | ---- | ----------- |
749+
| `ts` | string | Timestamp of the operation |
750+
| `issueIdOrKey` | string | Issue ID or key |
751+
| `forms` | array | List of forms attached to the issue |
752+
|`id` | string | Form instance ID \(UUID\) |
753+
|`name` | string | Form name |
754+
|`updated` | string | Last updated timestamp \(ISO 8601\) |
755+
|`submitted` | boolean | Whether the form has been submitted |
756+
|`lock` | boolean | Whether the form is locked |
757+
|`internal` | boolean | Whether the form is internal-only |
758+
|`formTemplateId` | string | Source form template ID \(UUID\) |
759+
| `total` | number | Total number of forms |
760+
681761

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6614,9 +6614,21 @@
66146614
{
66156615
"name": "Get Request Type Fields",
66166616
"description": "Get the fields required to create a request of a specific type in Jira Service Management"
6617+
},
6618+
{
6619+
"name": "Get Form Templates",
6620+
"description": "List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types"
6621+
},
6622+
{
6623+
"name": "Get Form Structure",
6624+
"description": "Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions"
6625+
},
6626+
{
6627+
"name": "Get Issue Forms",
6628+
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
66176629
}
66186630
],
6619-
"operationCount": 21,
6631+
"operationCount": 24,
66206632
"triggers": [],
66216633
"triggerCount": 0,
66226634
"authType": "oauth",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
5+
import {
6+
getJiraCloudId,
7+
getJsmFormsApiBaseUrl,
8+
getJsmHeaders,
9+
parseJsmErrorMessage,
10+
} from '@/tools/jsm/utils'
11+
12+
export const dynamic = 'force-dynamic'
13+
14+
const logger = createLogger('JsmIssueFormsAPI')
15+
16+
export async function POST(request: NextRequest) {
17+
const auth = await checkInternalAuth(request)
18+
if (!auth.success || !auth.userId) {
19+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
20+
}
21+
22+
try {
23+
const body = await request.json()
24+
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
25+
26+
if (!domain) {
27+
logger.error('Missing domain in request')
28+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
29+
}
30+
31+
if (!accessToken) {
32+
logger.error('Missing access token in request')
33+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
34+
}
35+
36+
if (!issueIdOrKey) {
37+
logger.error('Missing issueIdOrKey in request')
38+
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
39+
}
40+
41+
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
42+
43+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
44+
if (!cloudIdValidation.isValid) {
45+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
46+
}
47+
48+
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
49+
if (!issueIdOrKeyValidation.isValid) {
50+
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
51+
}
52+
53+
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
54+
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
55+
56+
logger.info('Fetching issue forms from:', { url, issueIdOrKey })
57+
58+
const response = await fetch(url, {
59+
method: 'GET',
60+
headers: getJsmHeaders(accessToken),
61+
})
62+
63+
if (!response.ok) {
64+
const errorText = await response.text()
65+
logger.error('JSM Forms API error:', {
66+
status: response.status,
67+
statusText: response.statusText,
68+
error: errorText,
69+
})
70+
71+
return NextResponse.json(
72+
{
73+
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
74+
details: errorText,
75+
},
76+
{ status: response.status }
77+
)
78+
}
79+
80+
const data = await response.json()
81+
82+
const forms = Array.isArray(data) ? data : (data.values ?? data.forms ?? [])
83+
84+
return NextResponse.json({
85+
success: true,
86+
output: {
87+
ts: new Date().toISOString(),
88+
issueIdOrKey,
89+
forms: forms.map((form: Record<string, unknown>) => ({
90+
id: form.id ?? null,
91+
name: form.name ?? null,
92+
updated: form.updated ?? null,
93+
submitted: form.submitted ?? false,
94+
lock: form.lock ?? false,
95+
internal: form.internal ?? null,
96+
formTemplateId: (form.formTemplate as Record<string, unknown>)?.id ?? null,
97+
})),
98+
total: forms.length,
99+
},
100+
})
101+
} catch (error) {
102+
logger.error('Error fetching issue forms:', {
103+
error: error instanceof Error ? error.message : String(error),
104+
stack: error instanceof Error ? error.stack : undefined,
105+
})
106+
107+
return NextResponse.json(
108+
{
109+
error: error instanceof Error ? error.message : 'Internal server error',
110+
success: false,
111+
},
112+
{ status: 500 }
113+
)
114+
}
115+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
5+
import {
6+
getJiraCloudId,
7+
getJsmFormsApiBaseUrl,
8+
getJsmHeaders,
9+
parseJsmErrorMessage,
10+
} from '@/tools/jsm/utils'
11+
12+
export const dynamic = 'force-dynamic'
13+
14+
const logger = createLogger('JsmFormStructureAPI')
15+
16+
export async function POST(request: NextRequest) {
17+
const auth = await checkInternalAuth(request)
18+
if (!auth.success || !auth.userId) {
19+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
20+
}
21+
22+
try {
23+
const body = await request.json()
24+
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey, formId } = body
25+
26+
if (!domain) {
27+
logger.error('Missing domain in request')
28+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
29+
}
30+
31+
if (!accessToken) {
32+
logger.error('Missing access token in request')
33+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
34+
}
35+
36+
if (!projectIdOrKey) {
37+
logger.error('Missing projectIdOrKey in request')
38+
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
39+
}
40+
41+
if (!formId) {
42+
logger.error('Missing formId in request')
43+
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
44+
}
45+
46+
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
47+
48+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
49+
if (!cloudIdValidation.isValid) {
50+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
51+
}
52+
53+
const projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
54+
if (!projectIdOrKeyValidation.isValid) {
55+
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
56+
}
57+
58+
const formIdValidation = validateJiraCloudId(formId, 'formId')
59+
if (!formIdValidation.isValid) {
60+
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
61+
}
62+
63+
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
64+
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form/${encodeURIComponent(formId)}`
65+
66+
logger.info('Fetching form template from:', { url, projectIdOrKey, formId })
67+
68+
const response = await fetch(url, {
69+
method: 'GET',
70+
headers: getJsmHeaders(accessToken),
71+
})
72+
73+
if (!response.ok) {
74+
const errorText = await response.text()
75+
logger.error('JSM Forms API error:', {
76+
status: response.status,
77+
statusText: response.statusText,
78+
error: errorText,
79+
})
80+
81+
return NextResponse.json(
82+
{
83+
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
84+
details: errorText,
85+
},
86+
{ status: response.status }
87+
)
88+
}
89+
90+
const data = await response.json()
91+
92+
return NextResponse.json({
93+
success: true,
94+
output: {
95+
ts: new Date().toISOString(),
96+
projectIdOrKey,
97+
formId,
98+
design: data.design ?? null,
99+
updated: data.updated ?? null,
100+
publish: data.publish ?? null,
101+
},
102+
})
103+
} catch (error) {
104+
logger.error('Error fetching form structure:', {
105+
error: error instanceof Error ? error.message : String(error),
106+
stack: error instanceof Error ? error.stack : undefined,
107+
})
108+
109+
return NextResponse.json(
110+
{
111+
error: error instanceof Error ? error.message : 'Internal server error',
112+
success: false,
113+
},
114+
{ status: 500 }
115+
)
116+
}
117+
}

0 commit comments

Comments
 (0)