Nettskjema MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Nettskjema MCP Server list my forms"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Nettskjema MCP Server
Model Context Protocol (MCP) server for the Nettskjema API v3. Provides tools to interact with Nettskjema forms, submissions, and data export through the MCP protocol.
Features
OAuth 2.1 Authentication: Secure client credentials flow with automatic token caching and refresh
Form Management: Create, list, retrieve, update, and delete forms
Form Elements: Get, update, and replace form elements (questions)
Submissions: Submit, retrieve, and manage form responses
Data Export: Export form responses as CSV, Excel, and SPSS
Attachments: Upload and retrieve submission attachments with transcription support
Invitations: Send reminders to pending invitations (creation handled in frontend)
User Information: Get authenticated user details
Type-Safe: Full type hints and validated against the OpenAPI spec
Related MCP server: formslist-mcp
Installation
Clone this repository and install with pip or uv:
git clone git@github.com:orjahren/nettskjema-mcp.git
cd nettskjema-mcp
pip install -e .Or with uv:
uv pip install -e .Configuration
1. Create API Client Credentials
Log into https://authorization.nettskjema.no/
Click "Register client"
Provide a client name and email
Save the clientSecret immediately - it's shown only once
Note your
clientIdandclientSecret
2. Configure Environment Variables
Create a .env file:
cp .env.example .envAdd your credentials:
NETTSKJEMA_CLIENT_ID=your_client_id_here
NETTSKJEMA_CLIENT_SECRET=your_client_secret_hereSecurity: Never commit your clientSecret. Store it in environment variables or a secure vault.
Token Management: OAuth access tokens are valid for 24 hours and are automatically cached and refreshed.
3. Grant Form Access
Your API client username is: <clientId>@apiclient
For each form you want to access:
Open the form in Nettskjema
Go to: Settings → Permissions → Editing permissions
Add your client username
Usage
Running the Server
After installation:
nettskjema-mcpOr as a Python module:
python -m nettskjema_mcpMCP Client Configuration
Add to your MCP client configuration (e.g., Claude Desktop):
{
"mcpServers": {
"nettskjema": {
"command": "nettskjema-mcp",
"env": {
"NETTSKJEMA_CLIENT_ID": "your_client_id_here",
"NETTSKJEMA_CLIENT_SECRET": "your_client_secret_here"
}
}
}
}Available Tools
Authentication & User
Tool | Description |
| Check OAuth authentication status and token info |
| Get authenticated user information |
Forms
Tool | Description |
| List all your forms |
| Create a new form (requires title and language) |
| Delete forms by ID (with trash retention) |
| Get metadata for a form |
| Get complete form definition |
| Get form configuration |
| Validate form settings without saving |
| Update form configuration |
| Find form ID by short title |
| Activate EduCloud integration |
| Activate TSD (Tjenester for Sensitive Data) |
| List forms that collect personal data |
Form Elements
Tool | Description |
| Get all elements in a form |
| Update a single element by ID |
| Replace entire element list (keep-list semantics) |
Note: Element tools use read-modify-write. See docs/workflows.md for details.
Submissions
Tool | Description |
| Submit a response to a form |
| Submit response using an invitation ID |
| Get a submission with answers |
| Get metadata for all submissions |
| Get metadata for postponed submissions |
| Get PDF rendering of a submission |
| List your submissions |
| Delete specific submissions by ID |
| Delete all submissions from a form |
Answers & Attachments
Tool | Description |
| Get all answers for a form |
| Download an attachment by ID |
| Upload file attachment to a submission |
| Get transcription of audio/video attachment |
| Request new transcription of attachment |
Data Export
Tool | Description |
| Export responses as CSV (semicolon delimiter) |
| Export responses as Excel file |
| Generate SPSS syntax file |
Codebooks & Images
Tool | Description |
| Upload a codebook for a form |
| Upload an image to Vortex |
| Retrieve an image by ID |
Invitations
Tool | Description |
| Send reminders to all pending invitations |
Invitation Creation Not Supported: The v2 invitation API is private and not available through this MCP server. To create invitations, use the frontend at https://nettskjema.no/user/form/{formId}/collect-responses/invited/create. Configure the form for invitations via MCP using update_form_settings with respondentGroup: "INVITED", then create invitations in the web UI. See docs/workflows.md for the complete workflow.
Documentation
API Coverage - Complete mapping of API routes to tools
Workflows - Common patterns and workflows
Contributing - Development guidelines
Testing
Run the test suite:
pytestRun with coverage:
pytest --cov=nettskjema_mcp --cov-report=htmlSee CONTRIBUTING.md for development setup and testing guidelines.
API Documentation
Full Nettskjema API documentation: https://nettskjema.no/api/v3/v3/api-docs
License
Copyright (C) 2026 Oliver Ruste Jahren
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Available Tools
38 toolsactivate_educloudA
Activate Educloud for a form.
The token parameter is a credential that authorizes activating Educloud for the named project. It must be kept as secure as the OAuth2 access token and is never logged or included in error messages.
The API returns 200 with no response body on success.
Inputs: token (required), form_id (required), project_name (required). Returns: result
activate_tsdB
Activate TSD (Services for Sensitive Data) for a form.
The token parameter is a credential that authorizes activating TSD for the named project. It must be kept as secure as the OAuth2 access token and is never logged or included in error messages.
The API returns 200 with no response body on success.
Inputs: token (required), form_id (required), project_name (required). Returns: result
check_auth_statusARead-only
Report which Nettskjema credentials and URLs this server is configured with.
Makes no request, so it still answers when authentication is the thing that is broken. The client secret is never included: only whether it is set.
Returns: A JSON object describing the current configuration, the names of any missing environment variables, and whether the server is ready to call the API.
Inputs: no named parameters. Returns: result
create_formA
Create a new form with the given settings and optional elements.
The six required settings are taken as named parameters so a model cannot omit them. Additional settings can be provided via extra_settings. If a required setting appears in extra_settings, the named parameter takes precedence.
Inputs: title (required), form_type (required), language_code (required), respondent_group (required), +4 more. Returns: result
delete_all_submissionsADestructive
Delete EVERY response the form has ever collected. This CANNOT be undone.
There is no id list and no partial form of this call: the whole dataset goes, for every respondent, and the API offers no undo and no trash can for submissions. Confirm with the user before calling it. To remove particular responses, use delete_submissions, which takes submission ids and leaves the rest of the form's data in place.
The spec's plural operation accepts an optional array body, but documents the operation as deleting all submissions and never says the ids narrow it. This tool therefore sends no body and offers no id parameter, rather than a parameter that looks like it limits the damage and may not.
Inputs: form_id (required). Returns: result
delete_formsADestructive
Delete one or more forms by their IDs.
Deleted forms are moved to the trash and can be restored for a limited time. This operation is destructive and cannot be undone after the trash retention period expires.
Inputs: form_ids (required). Returns: result
delete_submissionsADestructive
Delete ONLY the listed responses to a form. Other responses are left alone.
This is the scoped delete, and the one to use whenever specific responses are meant: it removes exactly the submissions whose ids are given, and nothing else. Do not confuse it with delete_all_submissions, which deletes every response the form has and takes no id list. Deleted responses are gone; the API offers no undo for them.
Get submission ids from get_submission_metadata or list_my_submissions. An empty list is refused rather than sent, because what the API would do with an empty body here is not specified and the cost of guessing wrong is a form's data.
Inputs: form_id (required), submission_ids (required). Returns: result
export_form_csvA
Export a form's submission data as CSV.
The spec's summary describes the response as a CSV text file with semicolon (;) as the delimiter. When output_path is provided, the file is written to disk (mode 0600 for a new file) and the result reports the path, byte count and media type. When output_path is omitted, the text is returned as JSON with a size signal and truncation flag. The result is capped at max_chars (default 1,000,000) to bound context size: check the truncated field to see if the full export was cut.
Inputs: form_id (required), max_chars, output_path, optimize_for_large_data. Returns: result
export_form_excelA
Export a form's submission data to a file, written to disk.
The spec documents this route's response as an Excel file. The file is streamed to disk and never inlined: a multi-megabyte spreadsheet as base64 is useless to a model. The returned path is absolute and tilde-expanded. An existing file at the output path is silently overwritten, and its mode is preserved. A new file (whether from output_path or a temp file) is created with mode 0600 (readable only by the current user).
When output_path is omitted, a temporary file is created in the system temp directory with a suffix derived from the response's media type. The temporary file persists after this call returns and is left on disk for the caller to use or delete.
Inputs: form_id (required), output_path, optimize_for_large_data. Returns: result
get_answer_attachmentA
Download an answer attachment to a file, written to disk.
The attachment is streamed to disk and never inlined: a multi-megabyte audio recording as base64 is useless to a model. The returned path is absolute and tilde-expanded. An existing file at the output path is silently overwritten, and its mode is preserved. A new file (whether from output_path or a temp file) is created with mode 0600 (readable only by the current user).
When output_path is omitted, a temporary file is created in the system temp directory with a suffix derived from the response's media type. The temporary file persists after this call returns and is left on disk for the caller to use or delete.
Inputs: answer_attachment_id (required), output_path. Returns: result
get_attachment_transcriptionARead-only
Get the transcription of an attachment in JSON, plain text, or WebVTT format.
The attachment must have been uploaded with transcription options for this to return meaningful results. A file uploaded without transcription, or a file that is not audio or video, has no transcription to retrieve.
For txt and vtt formats, the result is capped at max_chars (default 1,000,000) to bound context size. Check the truncated field to see if the full text was cut. The caller may raise this limit arbitrarily by passing a larger value.
Inputs: answer_attachment_id (required), format, max_chars. Returns: result
get_form_answersARead-only
Stream all answers for a form, as newline-delimited JSON.
This exports every answer on the form, one JSON object per line (NDJSON), so a large form's answers are never held whole in memory. Filter by element_ids to export only specific questions. The result is capped at max_rows (default 1000) to bound context size: check the returned truncated field to see if more data exists. When truncated is true, the reason field distinguishes a hit row cap ("rowCap") from a connection that closed mid-line ("incompleteFinalLine").
Inputs: form_id (required), max_rows, element_ids, optimize_for_large_data, +1 more. Returns: result
get_form_definitionCRead-only
Get the form's structure and creation history: the elements array and authorship metadata.
Inputs: form_id (required). Returns: result
get_form_elementsARead-only
Get the form's elements: its questions, headings, images and page breaks.
Use this before any element edit. The only write endpoint replaces the whole list, so the array returned here is the starting point for a modified list.
Inputs: form_id (required). Returns: result
get_form_infoBRead-only
Get operational metadata about a form: submission counts, open/closed status, and edit permissions.
Inputs: form_id (required). Returns: result
get_form_settingsBRead-only
Get the form's configuration: all 51 settings controlling form behavior and permissions.
Inputs: form_id (required), is_for_create_form. Returns: result
get_form_settings_validationCRead-only
Get validation errors or constraints for the form's settings.
Inputs: form_id (required). Returns: result
get_imageA
Download an image to a file, written to disk.
The image is streamed to disk and never inlined. The returned path is absolute and tilde-expanded. An existing file at the output path is silently overwritten, and its mode is preserved. A new file (whether from output_path or a temp file) is created with mode 0600 (readable only by the current user).
When output_path is omitted, a temporary file is created in the system temp directory with a suffix derived from the response's media type. The temporary file persists after this call returns and is left on disk for the caller to use or delete.
The spec declares the response as / (any media type), so the actual content type is reported as received.
Inputs: image_id (required), output_path. Returns: result
get_postponed_submission_metadataARead-only
Stream postponed submission metadata for a form, as newline-delimited JSON.
This endpoint returns the same metadata structure as get_submission_metadata, but only for postponed submissions. The result is streamed as NDJSON and capped at max_rows to bound context size.
Distinct from get_submission_metadata: this endpoint returns postponed submission metadata only, while that one returns all submission metadata for the form.
Inputs: form_id (required), max_rows, submission_ids_greater_than. Returns: result
get_spss_syntaxARead-only
Get SPSS syntax for a form's submission data.
The spec documents this route's response as an SPSS syntax file. The syntax is returned as JSON with a size signal and truncation flag. The result is capped at max_chars (default 1,000,000) to bound context size: check the truncated field to see if the full syntax was cut.
Inputs: form_id (required), max_chars. Returns: result
get_submissionBRead-only
Get a single submission with its answers and metadata.
Inputs: submission_id (required). Returns: result
get_submission_metadataARead-only
Stream all submission metadata for a form, as newline-delimited JSON.
Returns metadata without answers: use this for bulk export of submission-level fields or to find which submissions exist before fetching their answers. This streams NDJSON (one JSON object per line), so a large form's metadata is never held whole in memory. The result is capped at max_rows to bound context size: check the returned truncated field to see if more data exists.
Distinct from get_postponed_submission_metadata, which returns postponed submission metadata only. The relationship between the two result sets (whether this endpoint includes or excludes postponed submissions) is not specified by the API documentation.
Inputs: form_id (required), max_rows, optimize_for_large_data, submission_ids_greater_than. Returns: result
get_submission_pdfA
Download a submission as a PDF file.
The PDF is written to disk, not returned inline: a multi-megabyte PDF as base64 is not useful to a model. The returned path is absolute and tilde-expanded. An existing file at the output path is silently overwritten, and its mode is preserved. A new file (whether from output_path or a temp file) is created with mode 0600 (readable only by the current user).
When output_path is omitted, a temporary file is created in the system temp directory with a .pdf suffix. The temporary file persists after this call returns and is left on disk for the caller to use or delete.
Inputs: submission_id (required), output_path. Returns: result
get_user_infoBRead-only
Get the Nettskjema user record for the authenticated API client.
Returns: The user record as JSON.
Inputs: no named parameters. Returns: result
list_forms_collecting_personal_dataCRead-only
List forms that collect personal data, with optional filters.
Inputs: anonymous, inactive_personal_data_suspected. Returns: result
list_my_formsBRead-only
List all forms owned by the authenticated API client.
Returns: An array of form summary objects as JSON. Each object includes formId, title, open (boolean), modifiedDate, lastSubmissionDate, numberOfDeliveredSubmissions, deliveryDestination, and myFormsFormListingGroup.
Inputs: no named parameters. Returns: result
list_my_submissionsBRead-only
List all submissions made by the authenticated API client.
Returns: An array of submission summary objects as JSON. Each object includes submissionId, form (with title), createdDate, modifiedDate, and delivered (boolean).
Inputs: no named parameters. Returns: result
lookup_form_by_titleBRead-only
Look up a form by its short title (the URL-safe identifier).
Inputs: title_short (required). Returns: result
remind_all_pending_invitationsADestructive
Send reminder emails to every pending invitation on a form.
This operation is all-or-nothing: every sent invitation on the form receives a reminder. There is no endpoint to remind a subset of invitations. Whether it also reaches invitations created through the newer batch interface is not documented anywhere; they may share the same storage, but that is an inference and not something the API states.
Creating, viewing and deleting invitations are not available through this MCP server. Those operations are public Nettskjema features — supported, documented, production-ready — but are managed through the web frontend instead. Use the form management interface to handle invitations: https://nettskjema.no/user/form/{formId}/collect-responses/invited/create and https://nettskjema.no/user/form/{formId}/collect-responses/invited/overview
Inputs: form_id (required). Returns: result
replace_form_elementsADestructive
Replace the form's ENTIRE element list. Every element you leave out is DELETED.
This is not an "add an element" tool. Whatever you send becomes the form's
complete content: any question, heading, image or page break that is not in
elements is removed from the form. To add or change anything, call
get_form_elements first, modify the array it returns, and send all of it back
— or call update_form_element, which does that read-modify-write for one
element. Sending a one-element list to a ten-element form deletes nine
elements.
Nothing locks the form in between: an edit made by someone else after your get_form_elements call is overwritten by this one without warning. The endpoint offers no version field, ETag or conditional header to detect that.
Each element is checked against the vendored ElementDTO schema before the request is sent: elementType is required on every element and must be one of the known types, and every field the schema declares is checked for type and range. A field the schema does not declare is passed through to the API rather than rejected here, so an element read back from a newer API than the vendored snapshot can still be written back unchanged.
Inputs: form_id (required), elements (required). Returns: result
retranscribe_attachmentADestructive
Retranscribe an existing attachment with new transcription options.
The API overwrites the existing transcription. The attachment must be audio or video; retranscribing a non-media file or a file that was never transcribed has undefined effect.
The six supported models are: model_auto, nb_whisper_large_semantic, nb_whisper_large_verbatim, openai_whisper_large_v3, openai_whisper_large_v2, and KBLab-large. The language parameter accepts 100+ language codes; passing an unknown code raises BadRequestError naming the allowed values. The diarize flag requests speaker identification, and translate_to_english requests translation to English after transcription.
Inputs: answer_attachment_id (required), model, diarize, language, +1 more. Returns: result
submit_formA
Deliver a response to a form, as the authenticated API client.
This submits a real answer to a real form: it adds a row to the form's collected data, and the form's owner sees it alongside human responses. There is no draft or preview mode here. Use submit_form_for_invitation instead when answering on behalf of a specific invitation.
Each answer is checked against the vendored ClientAnswerDTO schema before the request is sent, so a bad enum value or a mistyped field fails without reaching the form. A field the snapshot does not declare is passed through to the API, which has the final say.
Inputs: answers (required), form_id (required), metadata. Returns: result
submit_form_for_invitationA
Deliver a response to the form a given invitation was issued for.
Same delivery as submit_form, addressed by invitation rather than by form: the response is recorded against that invitation, so the form's owner can see which invited respondent answered. The invitation is identified by its own numeric id, not by the form's. This submits a real answer to a real form; there is no draft or preview mode.
Each answer is checked against the vendored ClientAnswerDTO schema before the request is sent. A field the snapshot does not declare is passed through to the API, which has the final say.
Inputs: answers (required), invitation_id (required), metadata. Returns: result
update_form_elementADestructive
Change one element, by rewriting the form's ENTIRE element list.
The API has no single-element update, so this tool reads every element, applies
changes to the one whose elementId matches, and writes all of them back
through the same whole-list PUT that replace_form_elements uses. The other
elements are sent back exactly as they were read — but they are sent, so an
edit someone else makes between the read and the write is silently
overwritten. The operation is NOT atomic, and the endpoint offers no version
field, ETag or conditional header that would detect the conflict.
changes is merged shallowly, one level deep: each key replaces that key's
value on the element outright. Nested lists are NOT merged item by item —
passing answerOptions replaces the element's whole answer-option list, so to
edit one option you must send all of them, each keeping its answerOptionId,
in the order you want. The same goes for subElements and validationScripts.
Fetch the element with get_form_elements first if you do not already have
them.
If no element in the form has the given element_id, nothing is written: a PUT
of the unchanged list would report success while changing nothing. For the
same reason changes may not be empty, and may not carry elementId —
changing an element's own id does not move the edit to another element, it
asks the API to replace this one with a different element.
Inputs: changes (required), form_id (required), element_id (required). Returns: result
update_form_settingsA
Update one or more settings on an existing form.
This endpoint accepts partial updates: only the settings included in the settings dict will be updated. A 207 Multi-Status response means some settings were applied and others were rejected; this is reported as an error listing the failed settings.
The settings dict is validated as a partial FormSettingsDTO before the request is sent. Required-field constraints are relaxed for partial updates, but unknown keys are rejected locally to catch typos and flag fields that are newer than the vendored spec snapshot.
Inputs: form_id (required), settings (required), force_cleanup. Returns: result
upload_codebookADestructive
Upload a codebook mapping form elements to external identifiers.
The spec does not document whether omitted entries are preserved or deleted. Send the complete mapping you want the form to have: include every element, sub-element and answer option whose external id you want to keep or set. Omitting an entry has undefined effect and may delete that mapping.
Each map is validated against the CodebookDTO schema before the request is sent. All three maps are required by the schema: send an empty dict for any map you do not need to populate.
Inputs: form_id (required), element_ids_and_external_element_ids_map (required), sub_element_ids_and_external_element_ids_map (re… Returns: result
upload_imageB
Upload an image to the API, returning a response with string keys and values.
The entire file is read into memory, base64-encoded, and sent as a JSON string in the request body, per the published OpenAPI schema ({"type": "string", "format": "byte"} under application/json). The filename is derived from the file itself and sent as a query parameter.
The body format is most likely a Spring Boot serialisation artefact where a byte[] parameter was rendered as format: byte. The actual server may expect raw bytes; whether it accepts the JSON string can only be settled end-to-end.
Inputs: file_path (required). Returns: result
upload_submission_attachmentADestructive
Upload an attachment to a submission question, optionally requesting transcription.
The entire file is read into memory before uploading. The filename and fileSize are derived from the file itself rather than accepted as parameters, so they cannot drift out of sync. An audio recording awaiting transcription can be tens of megabytes; the whole file is held in RAM during the upload.
The attachment token comes from the attachmentToken field in the response body of POST /form/{formId}/submission. It authorizes writing to someone else's submission and must be kept as secure as the access token itself.
The spec does not document what happens when the named questionId already has an attachment. An upload may silently replace a respondent's file.
Transcription options apply only when the uploaded file is audio or video. The six supported models are: model_auto, nb_whisper_large_semantic, nb_whisper_large_verbatim, openai_whisper_large_v3, openai_whisper_large_v2, and KBLab-large. The language parameter accepts 100+ language codes; passing an unknown code raises BadRequestError naming the allowed values. The diarize flag requests speaker identification, and translate_to_english requests translation to English after transcription.
Inputs: file_path (required), question_id (required), submission_id (required), attachment_token (required), +4 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 38 tools
Despite 38 tools, purposes are sharply distinct and descriptions actively call out near-neighbors (e.g. delete_submissions vs delete_all_submissions, get_submission_metadata vs get_postponed_submission_metadata, replace_form_elements vs update_form_element). The cluster of get_form_info/settings/settings_validation/definition tools targets distinct facets (metadata, config, validation, structure) and is well differentiated in prose.
Near-perfect snake_case verb_noun convention throughout: get_*, list_*, create_*, update_*, delete_*, upload_*, export_*, submit_*, activate_*. Pluralization and scoping (delete_forms, delete_submissions, delete_all_submissions) is used meaningfully and consistently.
38 tools is heavy and exceeds the comfortable 15-tool band, spanning forms, elements, settings, submissions, attachments, transcription, exports, invitations, users, auth, and codebook. The domain is genuinely broad and most tools map to distinct endpoints, but the surface is large enough that consolidation (e.g. the several get_form_* readers) would help.
Core lifecycle is well covered: form create/read/delete, element read/replace/update, settings read/update/validate, submission create/get/list/bulk-export/delete, attachment upload/download/transcribe, and multiple export formats. Gaps remain in invitation management (only remind_all_pending_invitations; create/view/delete are acknowledged as unavailable) and codebook is upload-only with no update/delete or form rename.
Related MCP Connectors
Authenticated MCP access to WebMoney wallets, transactions, messages and invoices.
Forms for AI agents. Build, publish, and read forms and their replies from any MCP client.
MCP server for managing FormDesigner forms, fields, logic rules, themes, and entries
Managed MCP gateway with OAuth, per-agent permissions, shared accounts and activity logs.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables interaction with D2L Brightspace through the MCP protocol, supporting multi-strategy authentication and opt-in write operations.405 npm15MIT
- FlicenseAqualityCmaintenanceMCP server for FormsList, a form-backend SaaS. Enables creating form endpoints, reading submissions, configuring auto-responses, and generating embed snippets via natural language.7-
- AlicenseNot gradedqualityBmaintenanceEnables MCP-compatible clients to connect to any Frappe or Frappe HRMS site, exposing schema discovery, document CRUD and workflow operations, and HR helpers such as leave and attendance management through standardized tools.MIT
- AlicenseNot gradedqualityCmaintenanceEnables users to create forms, read submissions, and build and publish invitations from Claude, ChatGPT, Cursor, or any MCP client via a remote, OAuth-secured connection.MIT