Skip to main content
Glama

SharePoint Online MCP Server

by Zerg00s
createListField.ts14.4 kB
// src/tools/createListField.ts import request from 'request-promise'; import { IToolResult, IFieldUpdateData } from '../interfaces'; import { getSharePointHeaders, getRequestDigest } from '../auth_factory'; import { SharePointConfig } from '../config'; export interface CreateListFieldParams { url: string; listTitle: string; fieldData: { Title: string; // Display name for the field (can contain spaces) CleanName?: string; // Clean name without spaces (used for internal name generation) FieldTypeKind: number; // Field type values: 0=Invalid, 1=Integer, 2=Text, 3=Note, 4=DateTime, 5=Choice, 6=Lookup, 7=Boolean (docs), 8=Boolean, 9=Number, 10=Currency, 11=URL, 15=MultiChoice, 17=Calculated, 19=User Required?: boolean; EnforceUniqueValues?: boolean; StaticName?: string; // Static name, if not provided will be generated from CleanName or Title Description?: string; Choices?: string[]; // For choice fields DefaultValue?: string | number | boolean; [key: string]: any; // Any other field properties }; } /** * Create a new field (column) in a SharePoint list * @param params Parameters including site URL, list title, and field data * @param config SharePoint configuration * @returns Tool result with creation status and new field info */ export async function createListField( params: CreateListFieldParams, config: SharePointConfig ): Promise<IToolResult> { const { url, listTitle, fieldData } = params; console.error(`createListField tool called with URL: ${url}, List Title: ${listTitle}, Field Title: ${fieldData.Title}`); try { // Validate required parameters if (!fieldData.Title) { throw new Error("Field Title is required"); } if (fieldData.FieldTypeKind === undefined || fieldData.FieldTypeKind === null) { throw new Error("FieldTypeKind is required"); } // Authenticate with SharePoint const headers = await getSharePointHeaders(url, config); console.error("Headers prepared with authentication"); // Get request digest for POST operations headers['X-RequestDigest'] = await getRequestDigest(url, headers); headers['Content-Type'] = 'application/json;odata=verbose'; console.error("Headers prepared with request digest"); // Encode the list title to handle special characters const encodedListTitle = encodeURIComponent(listTitle); // First, verify the list exists console.error(`Verifying list "${listTitle}" exists...`); try { await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')`, headers: { ...headers, 'Content-Type': undefined }, json: true, method: 'GET', timeout: 15000 }); } catch (error) { throw new Error(`List "${listTitle}" not found`); } // Check if a field with the same title already exists console.error(`Checking if field "${fieldData.Title}" already exists...`); try { const fieldsResponse = await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')/fields?$filter=Title eq '${fieldData.Title}'`, headers: { ...headers, 'Content-Type': undefined }, json: true, method: 'GET', timeout: 30000 }); if (fieldsResponse.d.results && fieldsResponse.d.results.length > 0) { throw new Error(`A field with title "${fieldData.Title}" already exists in list "${listTitle}"`); } } catch (err: unknown) { // If the error is due to the field not being found, we can continue // Otherwise, re-throw the error const errorMessage = err instanceof Error ? err.message : String(err); if (errorMessage && !errorMessage.includes("already exists")) { throw err; } } // Determine the initial title to use (for internal name generation) // If CleanName is provided, use it for the initial creation const originalTitle = fieldData.Title; const temporaryTitle = fieldData.CleanName || fieldData.Title.replace(/\s/g, ''); console.error(`Using temporary title "${temporaryTitle}" for initial field creation to control internal name generation`); // Variable to hold the final field data let finalField: any; // Handle choice fields using CreateFieldAsXml for better reliability if (fieldData.FieldTypeKind === 5 || fieldData.FieldTypeKind === 15) { // Choice or MultiChoice if (!fieldData.Choices || !Array.isArray(fieldData.Choices) || fieldData.Choices.length === 0) { throw new Error("Choices array is required for Choice and MultiChoice fields"); } // Use the CreateFieldAsXml endpoint instead of the standard field creation console.error(`Creating Choice field using CreateFieldAsXml approach...`); // Build CHOICES XML element let choicesXml = "<CHOICES>"; for (const choice of fieldData.Choices) { choicesXml += `<CHOICE>${choice}</CHOICE>`; } choicesXml += "</CHOICES>"; // Build the full SchemaXml for the field const fieldDisplayName = originalTitle; const fieldName = temporaryTitle.replace(/\s/g, ''); // Set the field type based on FieldTypeKind const fieldType = fieldData.FieldTypeKind === 5 ? 'Choice' : 'MultiChoice'; const schemaXml = `<Field DisplayName='${fieldDisplayName}' FillInChoice='FALSE' IsModern='TRUE' Name='${fieldName}' Title='${fieldDisplayName}' Type='${fieldType}'>${choicesXml}</Field>`; const createXmlPayload = { parameters: { __metadata: { type: "SP.XmlSchemaFieldCreationInformation" }, SchemaXml: schemaXml, Options: 12 } }; console.error(`Creating field with XML payload: ${JSON.stringify(createXmlPayload)}`); try { // Use the CreateFieldAsXml endpoint const createXmlResponse = await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')/fields/CreateFieldAsXml`, headers: headers, json: true, method: 'POST', body: createXmlPayload, timeout: 30000 }); console.error(`Field created successfully with CreateFieldAsXml`); finalField = createXmlResponse.d; // Skip the standard field creation for Choice fields const newField = { Title: finalField.Title, InternalName: finalField.InternalName, StaticName: finalField.StaticName, Type: finalField.TypeAsString, Id: finalField.Id, Required: finalField.Required, Description: finalField.Description || '' }; return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `${fieldType} field "${originalTitle}" successfully created in list "${listTitle}" with internal name "${newField.InternalName}"`, newField: newField }, null, 2) }] } as IToolResult; } catch (xmlError) { console.error(`Error creating ${fieldType} field with CreateFieldAsXml: ${xmlError instanceof Error ? xmlError.message : String(xmlError)}`); console.error("Falling back to standard field creation method"); // Continue with standard field creation as fallback } } // Standard field creation approach for non-Choice fields or as fallback // Prepare the initial field creation payload - use CleanName or space-less Title for internal name generation const createPayload: any = { __metadata: { type: 'SP.Field' }, Title: temporaryTitle, FieldTypeKind: fieldData.FieldTypeKind }; // Add optional parameters if provided if (fieldData.Required !== undefined) { createPayload.Required = fieldData.Required; } if (fieldData.EnforceUniqueValues !== undefined) { createPayload.EnforceUniqueValues = fieldData.EnforceUniqueValues; } // If StaticName is provided, include it if (fieldData.StaticName) { createPayload.StaticName = fieldData.StaticName; } if (fieldData.Description) { createPayload.Description = fieldData.Description; } if (fieldData.DefaultValue !== undefined) { createPayload.DefaultValue = fieldData.DefaultValue.toString(); } // For choice fields (if CreateFieldAsXml failed) if (fieldData.FieldTypeKind === 5 || fieldData.FieldTypeKind === 15) { createPayload.Choices = { __metadata: { type: 'Collection(Edm.String)' }, results: fieldData.Choices }; } // Add any other properties from fieldData except Title/CleanName which we're handling specially Object.entries(fieldData).forEach(([key, value]) => { if (!createPayload[key] && key !== 'Choices' && key !== 'Title' && key !== 'CleanName') { createPayload[key] = value; } }); console.error(`Creating field with payload: ${JSON.stringify(createPayload)}`); // Step 1: Create the field with the temporary title const createResponse = await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')/Fields`, headers: headers, json: true, method: 'POST', body: createPayload, timeout: 30000 }); console.error(`Field created with internal name "${createResponse.d.InternalName}"`); // Start with the response data finalField = createResponse.d; // Check if we need to update the title (if we used a temporary title) if (temporaryTitle !== originalTitle) { console.error(`Updating field title from "${temporaryTitle}" to original title "${originalTitle}"`); // Get a fresh request digest for the update operation const updateHeaders = { ...headers }; updateHeaders['X-RequestDigest'] = await getRequestDigest(url, headers); updateHeaders['IF-MATCH'] = '*'; updateHeaders['X-HTTP-Method'] = 'MERGE'; // Step 2: Update the field title to the desired value with spaces const updatePayload = { __metadata: { type: 'SP.Field' }, Title: originalTitle }; // Encode the internal name to handle special characters const encodedInternalName = encodeURIComponent(createResponse.d.InternalName); await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')/fields/getByInternalNameOrTitle('${encodedInternalName}')`, headers: updateHeaders, json: true, method: 'POST', body: updatePayload, timeout: 20000 }); // Get the updated field information const getHeaders = { ...headers }; // Remove any headers that could cause issues with GET delete getHeaders['X-HTTP-Method']; delete getHeaders['IF-MATCH']; const updatedFieldResponse = await request({ url: `${url}/_api/web/lists/getByTitle('${encodedListTitle}')/fields/getByInternalNameOrTitle('${encodedInternalName}')`, headers: { ...getHeaders, 'Content-Type': undefined }, json: true, method: 'GET', timeout: 15000 }); finalField = updatedFieldResponse.d; } // Get the new field details const newField = { Title: finalField.Title, InternalName: finalField.InternalName, StaticName: finalField.StaticName, Type: finalField.TypeAsString, Id: finalField.Id, Required: finalField.Required, Description: finalField.Description || '' }; return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Field "${originalTitle}" successfully created in list "${listTitle}" with internal name "${newField.InternalName}"`, newField: newField }, null, 2) }] } as IToolResult; } catch (error: unknown) { // Type-safe error handling let errorMessage: string; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } else { errorMessage = "Unknown error occurred"; } console.error("Error in createListField tool:", errorMessage); return { content: [{ type: "text", text: `Error creating list field: ${errorMessage}` }], isError: true } as IToolResult; } } export default createListField;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Zerg00s/server-sharepoint'

If you have feedback or need assistance with the MCP directory API, please join our Discord server